[core] restructure to allow PIP packaging
OK - so I have to admit I *hate* the fact that PIP seems to require a subdirectory named like the library. But since the PIP package is something really nifty to have (thanks to @tony again!!!), I updated the codebase to hopefully conform with what PIP expects. Testruns so far look promising...
This commit is contained in:
parent
1d25be2059
commit
320827d577
146 changed files with 2509 additions and 2 deletions
0
bumblebee_status/__init__.py
Normal file
0
bumblebee_status/__init__.py
Normal file
520
bumblebee_status/_version.py
Normal file
520
bumblebee_status/_version.py
Normal 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}
|
0
bumblebee_status/core/__init__.py
Normal file
0
bumblebee_status/core/__init__.py
Normal file
240
bumblebee_status/core/config.py
Normal file
240
bumblebee_status/core/config.py
Normal 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
|
67
bumblebee_status/core/decorators.py
Normal file
67
bumblebee_status/core/decorators.py
Normal 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
|
29
bumblebee_status/core/event.py
Normal file
29
bumblebee_status/core/event.py
Normal 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
|
69
bumblebee_status/core/input.py
Normal file
69
bumblebee_status/core/input.py
Normal 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
|
124
bumblebee_status/core/module.py
Normal file
124
bumblebee_status/core/module.py
Normal 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
|
245
bumblebee_status/core/output.py
Normal file
245
bumblebee_status/core/output.py
Normal 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
|
158
bumblebee_status/core/theme.py
Normal file
158
bumblebee_status/core/theme.py
Normal 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
|
69
bumblebee_status/core/widget.py
Normal file
69
bumblebee_status/core/widget.py
Normal 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
|
9
bumblebee_status/discover.py
Normal file
9
bumblebee_status/discover.py
Normal 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
|
0
bumblebee_status/modules/__init__.py
Normal file
0
bumblebee_status/modules/__init__.py
Normal file
0
bumblebee_status/modules/contrib/__init__.py
Normal file
0
bumblebee_status/modules/contrib/__init__.py
Normal file
51
bumblebee_status/modules/contrib/amixer.py
Normal file
51
bumblebee_status/modules/contrib/amixer.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""get volume level
|
||||
|
||||
Parameters:
|
||||
* amixer.device: Device to use, defaults to "Master,0"
|
||||
|
||||
contributed by `zetxx <https://github.com/zetxx>`_ - many thanks!
|
||||
"""
|
||||
import re
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.volume))
|
||||
|
||||
self.__level = "n/a"
|
||||
self.__muted = True
|
||||
device = self.parameter("device", "Master,0")
|
||||
self._cmdString = "amixer get {}".format(device)
|
||||
|
||||
def volume(self, widget):
|
||||
if self.__level == "n/a":
|
||||
return self.__level
|
||||
m = re.search(r"([\d]+)\%", self.__level)
|
||||
self.__muted = True
|
||||
if m:
|
||||
if m.group(1) != "0" and "[on]" in self.__level:
|
||||
self.__muted = False
|
||||
return "{}%".format(m.group(1))
|
||||
else:
|
||||
return "0%"
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__level = util.cli.execute(
|
||||
"amixer get {}".format(self.parameter("device", "Master,0"))
|
||||
)
|
||||
except Exception as e:
|
||||
self.__level = "n/a"
|
||||
|
||||
def state(self, widget):
|
||||
if self.__muted:
|
||||
return ["warning", "muted"]
|
||||
return ["unmuted"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
89
bumblebee_status/modules/contrib/apt.py
Normal file
89
bumblebee_status/modules/contrib/apt.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays APT package update information (<to upgrade>/<to remove >)
|
||||
Requires the following packages:
|
||||
|
||||
* aptitude
|
||||
|
||||
contributed by `qba10 <https://github.com/qba10>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import re
|
||||
import threading
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
PATTERN = "{} packages upgraded, {} newly installed, {} to remove and {} not upgraded."
|
||||
|
||||
|
||||
def parse_result(to_parse):
|
||||
# We want to line with the iforamtion about package upgrade
|
||||
line_to_parse = to_parse.split("\n")[-4]
|
||||
result = re.search(
|
||||
"(.+) packages upgraded, (.+) newly installed, (.+) to remove", line_to_parse
|
||||
)
|
||||
|
||||
return int(result.group(1)), int(result.group(3))
|
||||
|
||||
|
||||
def get_apt_check_info(module):
|
||||
widget = module.widget()
|
||||
try:
|
||||
res = util.cli.execute("aptitude full-upgrade --simulate --assume-yes")
|
||||
widget.set("error", None)
|
||||
except (RuntimeError, FileNotFoundError) as e:
|
||||
widget.set("error", "unable to query APT: {}".format(e))
|
||||
return
|
||||
|
||||
to_upgrade = 0
|
||||
to_remove = 0
|
||||
try:
|
||||
to_upgrade, to_remove = parse_result(res)
|
||||
widget.set("to_upgrade", to_upgrade)
|
||||
widget.set("to_remove", to_remove)
|
||||
except Exception as e:
|
||||
widget.set("error", "parse error: {}".format(e))
|
||||
|
||||
core.event.trigger("update", [module.id], redraw_only=True)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||
self.__thread = None
|
||||
|
||||
def updates(self, widget):
|
||||
if widget.get("error"):
|
||||
return widget.get("error")
|
||||
return "{} to upgrade, {} to remove".format(
|
||||
widget.get("to_upgrade", 0), widget.get("to_remove", 0)
|
||||
)
|
||||
|
||||
def update(self):
|
||||
if self.__thread and self.__thread.isAlive():
|
||||
return
|
||||
|
||||
self.__thread = threading.Thread(target=get_apt_check_info, args=(self,))
|
||||
self.__thread.start()
|
||||
|
||||
def state(self, widget):
|
||||
cnt = 0
|
||||
ret = "good"
|
||||
for t in ["to_upgrade", "to_remove"]:
|
||||
cnt += widget.get(t, 0)
|
||||
if cnt > 50:
|
||||
ret = "critical"
|
||||
elif cnt > 0:
|
||||
ret = "warning"
|
||||
if widget.get("error"):
|
||||
ret = "critical"
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
50
bumblebee_status/modules/contrib/arch-update.py
Normal file
50
bumblebee_status/modules/contrib/arch-update.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
"""Check updates to Arch Linux.
|
||||
|
||||
Requires the following executable:
|
||||
* checkupdates (from pacman-contrib)
|
||||
|
||||
contributed by `lucassouto <https://github.com/lucassouto>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
||||
self.__packages = 0
|
||||
self.__error = False
|
||||
|
||||
@property
|
||||
def __format(self):
|
||||
return self.parameter("format", "Update Arch: {}")
|
||||
|
||||
def utilization(self, widget):
|
||||
return self.__format.format(self.__packages)
|
||||
|
||||
def hidden(self):
|
||||
return self.__packages == 0 and not self.__error
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
result = util.cli.execute("checkupdates")
|
||||
self.__packages = len(result.split("\n")) - 1
|
||||
self.__error = False
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.__error = True
|
||||
|
||||
def state(self, widget):
|
||||
if self.__error:
|
||||
return "warning"
|
||||
return self.threshold_state(self.__packages, 1, 100)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
334
bumblebee_status/modules/contrib/battery-upower.py
Normal file
334
bumblebee_status/modules/contrib/battery-upower.py
Normal file
|
@ -0,0 +1,334 @@
|
|||
# UPowerManger Class Copyright (C) 2017 Oscar Svensson (wogscpar) under MIT licence from upower-python
|
||||
|
||||
"""Displays battery status, remaining percentage and charging information.
|
||||
|
||||
Parameters:
|
||||
* battery-upower.warning : Warning threshold in % of remaining charge (defaults to 20)
|
||||
* battery-upower.critical : Critical threshold in % of remaining charge (defaults to 10)
|
||||
* battery-upower.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged
|
||||
|
||||
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import dbus
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
class UPowerManager:
|
||||
def __init__(self):
|
||||
self.UPOWER_NAME = "org.freedesktop.UPower"
|
||||
self.UPOWER_PATH = "/org/freedesktop/UPower"
|
||||
|
||||
self.DBUS_PROPERTIES = "org.freedesktop.DBus.Properties"
|
||||
self.bus = dbus.SystemBus()
|
||||
|
||||
def detect_devices(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
|
||||
|
||||
devices = upower_interface.EnumerateDevices()
|
||||
return devices
|
||||
|
||||
def get_display_device(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
|
||||
|
||||
dispdev = upower_interface.GetDisplayDevice()
|
||||
return dispdev
|
||||
|
||||
def get_critical_action(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME)
|
||||
|
||||
critical_action = upower_interface.GetCriticalAction()
|
||||
return critical_action
|
||||
|
||||
def get_device_percentage(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
return battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Percentage")
|
||||
|
||||
def get_full_device_information(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
hasHistory = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "HasHistory"
|
||||
)
|
||||
hasStatistics = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "HasStatistics"
|
||||
)
|
||||
isPresent = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "IsPresent"
|
||||
)
|
||||
isRechargable = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "IsRechargeable"
|
||||
)
|
||||
online = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Online")
|
||||
powersupply = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "PowerSupply"
|
||||
)
|
||||
capacity = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Capacity")
|
||||
energy = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Energy")
|
||||
energyempty = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "EnergyEmpty"
|
||||
)
|
||||
energyfull = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "EnergyFull"
|
||||
)
|
||||
energyfulldesign = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "EnergyFullDesign"
|
||||
)
|
||||
energyrate = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "EnergyRate"
|
||||
)
|
||||
luminosity = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "Luminosity"
|
||||
)
|
||||
percentage = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "Percentage"
|
||||
)
|
||||
temperature = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "Temperature"
|
||||
)
|
||||
voltage = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Voltage")
|
||||
timetoempty = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "TimeToEmpty"
|
||||
)
|
||||
timetofull = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "TimeToFull"
|
||||
)
|
||||
iconname = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "IconName")
|
||||
model = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Model")
|
||||
nativepath = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "NativePath"
|
||||
)
|
||||
serial = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Serial")
|
||||
vendor = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Vendor")
|
||||
state = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State")
|
||||
technology = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "Technology"
|
||||
)
|
||||
battype = battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "Type")
|
||||
warninglevel = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "WarningLevel"
|
||||
)
|
||||
updatetime = battery_proxy_interface.Get(
|
||||
self.UPOWER_NAME + ".Device", "UpdateTime"
|
||||
)
|
||||
|
||||
information_table = {
|
||||
"HasHistory": hasHistory,
|
||||
"HasStatistics": hasStatistics,
|
||||
"IsPresent": isPresent,
|
||||
"IsRechargeable": isRechargable,
|
||||
"Online": online,
|
||||
"PowerSupply": powersupply,
|
||||
"Capacity": capacity,
|
||||
"Energy": energy,
|
||||
"EnergyEmpty": energyempty,
|
||||
"EnergyFull": energyfull,
|
||||
"EnergyFullDesign": energyfulldesign,
|
||||
"EnergyRate": energyrate,
|
||||
"Luminosity": luminosity,
|
||||
"Percentage": percentage,
|
||||
"Temperature": temperature,
|
||||
"Voltage": voltage,
|
||||
"TimeToEmpty": timetoempty,
|
||||
"TimeToFull": timetofull,
|
||||
"IconName": iconname,
|
||||
"Model": model,
|
||||
"NativePath": nativepath,
|
||||
"Serial": serial,
|
||||
"Vendor": vendor,
|
||||
"State": state,
|
||||
"Technology": technology,
|
||||
"Type": battype,
|
||||
"WarningLevel": warninglevel,
|
||||
"UpdateTime": updatetime,
|
||||
}
|
||||
|
||||
return information_table
|
||||
|
||||
def is_lid_present(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
is_lid_present = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsPresent"))
|
||||
return is_lid_present
|
||||
|
||||
def is_lid_closed(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
is_lid_closed = bool(upower_interface.Get(self.UPOWER_NAME, "LidIsClosed"))
|
||||
return is_lid_closed
|
||||
|
||||
def on_battery(self):
|
||||
upower_proxy = self.bus.get_object(self.UPOWER_NAME, self.UPOWER_PATH)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
on_battery = bool(upower_interface.Get(self.UPOWER_NAME, "OnBattery"))
|
||||
return on_battery
|
||||
|
||||
def has_wakeup_capabilities(self):
|
||||
upower_proxy = self.bus.get_object(
|
||||
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
|
||||
)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
has_wakeup_capabilities = bool(
|
||||
upower_interface.Get(self.UPOWER_NAME + ".Wakeups", "HasCapability")
|
||||
)
|
||||
return has_wakeup_capabilities
|
||||
|
||||
def get_wakeups_data(self):
|
||||
upower_proxy = self.bus.get_object(
|
||||
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
|
||||
)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups")
|
||||
|
||||
data = upower_interface.GetData()
|
||||
return data
|
||||
|
||||
def get_wakeups_total(self):
|
||||
upower_proxy = self.bus.get_object(
|
||||
self.UPOWER_NAME, self.UPOWER_PATH + "/Wakeups"
|
||||
)
|
||||
upower_interface = dbus.Interface(upower_proxy, self.UPOWER_NAME + ".Wakeups")
|
||||
|
||||
data = upower_interface.GetTotal()
|
||||
return data
|
||||
|
||||
def is_loading(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State"))
|
||||
|
||||
if state == 1:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_state(self, battery):
|
||||
battery_proxy = self.bus.get_object(self.UPOWER_NAME, battery)
|
||||
battery_proxy_interface = dbus.Interface(battery_proxy, self.DBUS_PROPERTIES)
|
||||
|
||||
state = int(battery_proxy_interface.Get(self.UPOWER_NAME + ".Device", "State"))
|
||||
|
||||
if state == 0:
|
||||
return "Unknown"
|
||||
elif state == 1:
|
||||
return "Loading"
|
||||
elif state == 2:
|
||||
return "Discharging"
|
||||
elif state == 3:
|
||||
return "Empty"
|
||||
elif state == 4:
|
||||
return "Fully charged"
|
||||
elif state == 5:
|
||||
return "Pending charge"
|
||||
elif state == 6:
|
||||
return "Pending discharge"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.capacity))
|
||||
|
||||
try:
|
||||
self.power = UPowerManager()
|
||||
self.device = self.power.get_display_device()
|
||||
except Exception as e:
|
||||
logging.exception("unable to get battery display device: {}".format(str(e)))
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics"
|
||||
)
|
||||
|
||||
self._showremaining = util.format.asbool(self.parameter("showremaining", True))
|
||||
|
||||
def capacity(self, widget):
|
||||
widget.set("capacity", -1)
|
||||
widget.set("ac", False)
|
||||
output = "n/a"
|
||||
try:
|
||||
capacity = int(self.power.get_device_percentage(self.device))
|
||||
capacity = capacity if capacity < 100 else 100
|
||||
widget.set("capacity", capacity)
|
||||
output = "{}%".format(capacity)
|
||||
widget.set("theme.minwidth", "100%")
|
||||
except Exception as e:
|
||||
logging.exception("unable to get battery capacity: {}".format(str(e)))
|
||||
|
||||
if self._showremaining:
|
||||
try:
|
||||
p = self.power # an alias to make each line of code shorter
|
||||
proxy = p.bus.get_object(p.UPOWER_NAME, self.device)
|
||||
interface = dbus.Interface(proxy, p.DBUS_PROPERTIES)
|
||||
state = int(interface.Get(p.UPOWER_NAME + ".Device", "State"))
|
||||
# state: 1 => charging, 2 => discharging, other => don't care
|
||||
remain = int(
|
||||
interface.Get(
|
||||
p.UPOWER_NAME + ".Device",
|
||||
["TimeToFull", "TimeToEmpty"][state - 1],
|
||||
)
|
||||
)
|
||||
remain = util.format.duration(remain, compact=True, unit=True)
|
||||
output = "{} {}".format(output, remain)
|
||||
except IndexError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.exception(
|
||||
"unable to get battery remaining time: {}".format(str(e))
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
capacity = widget.get("capacity", -1)
|
||||
if capacity < 0:
|
||||
return ["critical", "unknown"]
|
||||
|
||||
if capacity < int(self.parameter("critical", 10)):
|
||||
state.append("critical")
|
||||
elif capacity < int(self.parameter("warning", 20)):
|
||||
state.append("warning")
|
||||
|
||||
if widget.get("ac"):
|
||||
state.append("AC")
|
||||
else:
|
||||
charge = "Unknown"
|
||||
try:
|
||||
charge = self.power.get_state(self.device)
|
||||
except Exception as e:
|
||||
logging.exception("unable to get charge value: {}".format(str(e)))
|
||||
if charge == "Discharging":
|
||||
state.append(
|
||||
"discharging-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
elif charge == "Unknown":
|
||||
state.append(
|
||||
"unknown-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
else:
|
||||
if capacity > 95:
|
||||
state.append("charged")
|
||||
else:
|
||||
state.append("charging")
|
||||
return state
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
209
bumblebee_status/modules/contrib/battery.py
Normal file
209
bumblebee_status/modules/contrib/battery.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays battery status, remaining percentage and charging information.
|
||||
|
||||
Parameters:
|
||||
* battery.device : Comma-separated list of battery devices to read information from (defaults to auto for auto-detection)
|
||||
* battery.warning : Warning threshold in % of remaining charge (defaults to 20)
|
||||
* battery.critical : Critical threshold in % of remaining charge (defaults to 10)
|
||||
* battery.showdevice : If set to 'true', add the device name to the widget (defaults to False)
|
||||
* battery.decorate : If set to 'false', hides additional icons (charging, etc.) (defaults to True)
|
||||
* battery.showpowerconsumption: If set to 'true', show current power consumption (defaults to False)
|
||||
* battery.compact-devices : If set to 'true', compacts multiple batteries into a single entry (default to False)
|
||||
|
||||
(partially) contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import power
|
||||
except ImportError:
|
||||
log.warning('unable to import module "power": Time estimates will not be available')
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
class BatteryManager(object):
|
||||
def remaining(self):
|
||||
try:
|
||||
estimate = power.PowerManagement().get_time_remaining_estimate()
|
||||
# do not show remaining if on AC
|
||||
if estimate == power.common.TIME_REMAINING_UNLIMITED:
|
||||
return None
|
||||
return estimate * 60 # return value in seconds
|
||||
except Exception as e:
|
||||
return -1
|
||||
return -1
|
||||
|
||||
def read(self, battery, component, default=None):
|
||||
path = "/sys/class/power_supply/{}".format(battery)
|
||||
if not os.path.exists(path):
|
||||
return default
|
||||
try:
|
||||
with open("{}/{}".format(path, component)) as f:
|
||||
return f.read().strip()
|
||||
except IOError:
|
||||
return "n/a"
|
||||
return default
|
||||
|
||||
def capacity(self, battery):
|
||||
capacity = self.read(battery, "capacity", 100)
|
||||
if capacity != "n/a":
|
||||
capacity = int(capacity)
|
||||
|
||||
return capacity if capacity < 100 else 100
|
||||
|
||||
def capacity_all(self, batteries):
|
||||
now = 0
|
||||
full = 0
|
||||
for battery in batteries:
|
||||
try:
|
||||
with open(
|
||||
"/sys/class/power_supply/{}/energy_full".format(battery)
|
||||
) as f:
|
||||
full += int(f.read())
|
||||
with open("/sys/class/power_supply/{}/energy_now".format(battery)) as f:
|
||||
now += int(f.read())
|
||||
except IOError:
|
||||
return "n/a"
|
||||
return int(float(now) / float(full) * 100.0)
|
||||
|
||||
def isac(self, battery):
|
||||
path = "/sys/class/power_supply/{}".format(battery)
|
||||
return not os.path.exists(path)
|
||||
|
||||
def isac_any(self, batteries):
|
||||
for battery in batteries:
|
||||
if self.isac(battery):
|
||||
return True
|
||||
return False
|
||||
|
||||
def consumption(self, battery):
|
||||
consumption = self.read(battery, "power_now", "n/a")
|
||||
if consumption == "n/a":
|
||||
return "n/a"
|
||||
return "{}W".format(int(consumption) / 1000000)
|
||||
|
||||
def charge(self, battery):
|
||||
return self.read(battery, "status", "n/a")
|
||||
|
||||
def charge_any(self, batteries):
|
||||
for battery in batteries:
|
||||
if self.charge(battery) == "Discharging":
|
||||
return "Discharging"
|
||||
return "Charged"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__manager = BatteryManager()
|
||||
|
||||
self._batteries = util.format.aslist(self.parameter("device", "auto"))
|
||||
if self._batteries[0] == "auto":
|
||||
self._batteries = [
|
||||
os.path.basename(battery)
|
||||
for battery in glob.glob("/sys/class/power_supply/BAT*")
|
||||
]
|
||||
if len(self._batteries) == 0:
|
||||
raise Exceptions("no batteries configured/found")
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics"
|
||||
)
|
||||
|
||||
if util.format.asbool(self.parameter("compact-devices", False)):
|
||||
widget = self.add_widget(
|
||||
full_text=self.capacity, name="all-batteries"
|
||||
)
|
||||
else:
|
||||
for battery in self._batteries:
|
||||
log.debug("adding new widget for {}".format(battery))
|
||||
widget = self.add_widget(
|
||||
full_text=self.capacity, name=battery
|
||||
)
|
||||
for w in self.widgets():
|
||||
if util.format.asbool(self.parameter("decorate", True)) == False:
|
||||
widget.set("theme.exclude", "suffix")
|
||||
|
||||
def capacity(self, widget):
|
||||
if widget.name == "all-batteries":
|
||||
capacity = self.__manager.capacity_all(self._batteries)
|
||||
else:
|
||||
capacity = self.__manager.capacity(widget.name)
|
||||
widget.set("capacity", capacity)
|
||||
widget.set("ac", self.__manager.isac_any(self._batteries))
|
||||
widget.set("theme.minwidth", "100%")
|
||||
|
||||
# Read power conumption
|
||||
if util.format.asbool(self.parameter("showpowerconsumption", False)):
|
||||
output = "{}% ({})".format(
|
||||
capacity, self.__manager.consumption(widget.name)
|
||||
)
|
||||
else:
|
||||
output = "{}%".format(capacity)
|
||||
|
||||
if (
|
||||
util.format.asbool(self.parameter("showremaining", True))
|
||||
and self.__manager.charge(widget.name) == "Discharging"
|
||||
):
|
||||
remaining = self.__manager.remaining()
|
||||
if remaining >= 0:
|
||||
output = "{} {}".format(
|
||||
output, util.format.duration(remaining, compact=True, unit=True)
|
||||
)
|
||||
|
||||
if util.format.asbool(self.parameter("showdevice", False)):
|
||||
output = "{} ({})".format(output, widget.name)
|
||||
|
||||
return output
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
capacity = widget.get("capacity")
|
||||
|
||||
if capacity < 0:
|
||||
log.debug("battery state: {}".format(state))
|
||||
return ["critical", "unknown"]
|
||||
|
||||
if capacity < int(self.parameter("critical", 10)):
|
||||
state.append("critical")
|
||||
elif capacity < int(self.parameter("warning", 20)):
|
||||
state.append("warning")
|
||||
|
||||
if widget.get("ac"):
|
||||
state.append("AC")
|
||||
else:
|
||||
if widget.name == "all-batteries":
|
||||
charge = self.__manager.charge_any(self._batteries)
|
||||
else:
|
||||
charge = self.__manager.charge(widget.name)
|
||||
if charge == "Discharging":
|
||||
state.append(
|
||||
"discharging-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
elif charge == "Unknown":
|
||||
state.append(
|
||||
"unknown-{}".format(
|
||||
min([10, 25, 50, 80, 100], key=lambda i: abs(i - capacity))
|
||||
)
|
||||
)
|
||||
else:
|
||||
if capacity > 95:
|
||||
state.append("charged")
|
||||
else:
|
||||
state.append("charging")
|
||||
return state
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
125
bumblebee_status/modules/contrib/bluetooth.py
Normal file
125
bumblebee_status/modules/contrib/bluetooth.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""Displays bluetooth status (Bluez). Left mouse click launches manager app,
|
||||
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state.
|
||||
|
||||
Parameters:
|
||||
* bluetooth.device : the device to read state from (default is hci0)
|
||||
* bluetooth.manager : application to launch on click (blueman-manager)
|
||||
* bluetooth.dbus_destination : dbus destination (defaults to org.blueman.Mechanism)
|
||||
* bluetooth.dbus_destination_path : dbus destination path (defaults to /)
|
||||
* bluetooth.right_click_popup : use popup menu when right-clicked (defaults to True)
|
||||
|
||||
contributed by `brunosmmm <https://github.com/brunosmmm>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
import util.popup
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.status))
|
||||
|
||||
device = self.parameter("device", "hci0")
|
||||
self.manager = self.parameter("manager", "blueman-manager")
|
||||
self._path = "/sys/class/bluetooth/{}".format(device)
|
||||
self._status = "Off"
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager)
|
||||
|
||||
# determine whether to use pop-up menu or simply toggle the device on/off
|
||||
right_click_popup = util.format.asbool(
|
||||
self.parameter("right_click_popup", True)
|
||||
)
|
||||
|
||||
if right_click_popup:
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.popup)
|
||||
else:
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle)
|
||||
|
||||
def status(self, widget):
|
||||
"""Get status."""
|
||||
return self._status
|
||||
|
||||
def update(self):
|
||||
"""Update current state."""
|
||||
if not os.path.exists(self._path):
|
||||
self._status = "?"
|
||||
return
|
||||
|
||||
# search for whichever rfkill directory available
|
||||
try:
|
||||
dirnames = next(os.walk(self._path))[1]
|
||||
for dirname in dirnames:
|
||||
m = re.match(r"rfkill[0-9]+", dirname)
|
||||
if m is not None:
|
||||
with open(os.path.join(self._path, dirname, "state"), "r") as f:
|
||||
state = int(f.read())
|
||||
if state == 1:
|
||||
self._status = "On"
|
||||
else:
|
||||
self._status = "Off"
|
||||
return
|
||||
|
||||
except IOError:
|
||||
self._status = "?"
|
||||
|
||||
def popup(self, widget):
|
||||
"""Show a popup menu."""
|
||||
menu = util.popup.PopupMenu()
|
||||
if self._status == "On":
|
||||
menu.add_menuitem("Disable Bluetooth")
|
||||
elif self._status == "Off":
|
||||
menu.add_menuitem("Enable Bluetooth")
|
||||
else:
|
||||
return
|
||||
|
||||
# show menu and get return code
|
||||
ret = menu.show(widget)
|
||||
if ret == 0:
|
||||
# first (and only) item selected.
|
||||
self._toggle()
|
||||
|
||||
def _toggle(self, widget=None):
|
||||
"""Toggle bluetooth state."""
|
||||
if self._status == "On":
|
||||
state = "false"
|
||||
else:
|
||||
state = "true"
|
||||
|
||||
dst = self.parameter("dbus_destination", "org.blueman.Mechanism")
|
||||
dst_path = self.parameter("dbus_destination_path", "/")
|
||||
|
||||
cmd = (
|
||||
"dbus-send --system --print-reply --dest={}"
|
||||
" {} org.blueman.Mechanism.SetRfkillState"
|
||||
" boolean:{}".format(dst, dst_path, state)
|
||||
)
|
||||
|
||||
logging.debug("bt: toggling bluetooth")
|
||||
util.cli.execute(cmd)
|
||||
|
||||
def state(self, widget):
|
||||
"""Get current state."""
|
||||
state = []
|
||||
|
||||
if self._status == "?":
|
||||
state = ["unknown"]
|
||||
elif self._status == "On":
|
||||
state = ["ON"]
|
||||
else:
|
||||
state = ["OFF"]
|
||||
|
||||
return state
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
103
bumblebee_status/modules/contrib/bluetooth2.py
Normal file
103
bumblebee_status/modules/contrib/bluetooth2.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
"""Displays bluetooth status. Left mouse click launches manager app,
|
||||
right click toggles bluetooth. Needs dbus-send to toggle bluetooth state and
|
||||
python-dbus to count the number of connections
|
||||
|
||||
Parameters:
|
||||
* bluetooth.manager : application to launch on click (blueman-manager)
|
||||
|
||||
contributed by `martindoublem <https://github.com/martindoublem>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import dbus
|
||||
import dbus.mainloop.glib
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.status))
|
||||
|
||||
self.manager = self.parameter("manager", "blueman-manager")
|
||||
self._status = "Off"
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
self._bus = dbus.SystemBus()
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.manager)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self._toggle)
|
||||
|
||||
def status(self, widget):
|
||||
"""Get status."""
|
||||
return self._status
|
||||
|
||||
def update(self):
|
||||
"""Update current state."""
|
||||
state = len(
|
||||
subprocess.run(["bluetoothctl", "list"], stdout=subprocess.PIPE).stdout
|
||||
)
|
||||
if state > 0:
|
||||
connected_devices = self.get_connected_devices()
|
||||
self._status = "On - {}".format(connected_devices)
|
||||
else:
|
||||
self._status = "Off"
|
||||
adapters_cmd = "rfkill list | grep Bluetooth"
|
||||
if not len(
|
||||
subprocess.run(adapters_cmd, shell=True, stdout=subprocess.PIPE).stdout
|
||||
):
|
||||
self._status = "No Adapter Found"
|
||||
return
|
||||
|
||||
def _toggle(self, widget=None):
|
||||
"""Toggle bluetooth state."""
|
||||
if "On" in self._status:
|
||||
state = "false"
|
||||
else:
|
||||
state = "true"
|
||||
|
||||
cmd = (
|
||||
"dbus-send --system --print-reply --dest=org.blueman.Mechanism /org/blueman/mechanism org.blueman.Mechanism.SetRfkillState boolean:%s"
|
||||
% state
|
||||
)
|
||||
|
||||
logging.debug("bt: toggling bluetooth")
|
||||
core.util.execute(cmd)
|
||||
|
||||
def state(self, widget):
|
||||
"""Get current state."""
|
||||
state = []
|
||||
|
||||
if self._status == "No Adapter Found":
|
||||
state.append("critical")
|
||||
elif self._status == "On - 0":
|
||||
state.append("warning")
|
||||
elif "On" in self._status and not (self._status == "On - 0"):
|
||||
state.append("ON")
|
||||
else:
|
||||
state.append("critical")
|
||||
return state
|
||||
|
||||
def get_connected_devices(self):
|
||||
devices = 0
|
||||
objects = dbus.Interface(
|
||||
self._bus.get_object("org.bluez", "/"), "org.freedesktop.DBus.ObjectManager"
|
||||
).GetManagedObjects()
|
||||
for path, interfaces in objects.items():
|
||||
if "org.bluez.Device1" in interfaces:
|
||||
if dbus.Interface(
|
||||
self._bus.get_object("org.bluez", path),
|
||||
"org.freedesktop.DBus.Properties",
|
||||
).Get("org.bluez.Device1", "Connected"):
|
||||
devices += 1
|
||||
return devices
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
70
bumblebee_status/modules/contrib/brightness.py
Normal file
70
bumblebee_status/modules/contrib/brightness.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the brightness of a display
|
||||
|
||||
Parameters:
|
||||
* brightness.step: The amount of increase/decrease on scroll in % (defaults to 2)
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.brightness))
|
||||
|
||||
self.__brightness = "n/a"
|
||||
self.__readcmd = None
|
||||
step = self.parameter("step", 2)
|
||||
|
||||
if shutil.which("light"):
|
||||
self.__readcmd = self.__light
|
||||
self.register_cmd("light -A {}%".format(step), "light -U {}%".format(step))
|
||||
elif shutil.which("brightnessctl"):
|
||||
self.__readcmd = self.__brightnessctl
|
||||
self.register_cmd(
|
||||
"brightnessctl s {}%+".format(step), "brightnessctl s {}%-".format(step)
|
||||
)
|
||||
else:
|
||||
self.__readcmd = self.__xbacklight
|
||||
self.register_cmd(
|
||||
"xbacklight +{}%".format(step), "xbacklight -{}%".format(step)
|
||||
)
|
||||
|
||||
def register_cmd(self, up_cmd, down_cmd):
|
||||
core.input.register(self, button=core.input.WHEEL_UP, cmd=up_cmd)
|
||||
core.input.register(self, button=core.input.WHEEL_DOWN, cmd=down_cmd)
|
||||
|
||||
def brightness(self, widget):
|
||||
return self.__brightness
|
||||
|
||||
def __light(self):
|
||||
return util.cli.execute("light").strip()
|
||||
|
||||
def __brightnessctl(self):
|
||||
m = util.cli.execute("brightnessctl m").strip()
|
||||
g = util.cli.execute("brightnessctl g").strip()
|
||||
return float(g) / float(m) * 100.0
|
||||
|
||||
def __xbacklight(self):
|
||||
return util.cli.execute("xbacklight -get").strip()
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__brightness = "{:3.0f}%".format(float(self.__readcmd()))
|
||||
except:
|
||||
self.__brightness = "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
117
bumblebee_status/modules/contrib/caffeine.py
Normal file
117
bumblebee_status/modules/contrib/caffeine.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# pylint: disable=C0111,R0903,W0212
|
||||
|
||||
"""Enable/disable automatic screen locking.
|
||||
|
||||
Requires the following executables:
|
||||
* xdg-screensaver
|
||||
* xdotool
|
||||
* xprop (as dependency for xdotool)
|
||||
* notify-send
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import psutil
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=10)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(""))
|
||||
|
||||
self.__active = False
|
||||
self.__xid = None
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__toggle)
|
||||
|
||||
def __check_requirements(self):
|
||||
requirements = ["xdotool", "xprop", "xdg-screensaver"]
|
||||
missing = []
|
||||
for tool in requirements:
|
||||
if not shutil.which(tool):
|
||||
missing.append(tool)
|
||||
return missing
|
||||
|
||||
def __get_i3bar_xid(self):
|
||||
xid = (
|
||||
util.cli.execute("xdotool search --class 'i3bar'")
|
||||
.partition("\n")[0]
|
||||
.strip()
|
||||
)
|
||||
if xid.isdigit():
|
||||
return xid
|
||||
logging.warning("Module caffeine: xdotool couldn't get X window ID of 'i3bar'.")
|
||||
return None
|
||||
|
||||
def __notify(self):
|
||||
if not shutil.which("notify-send"):
|
||||
return
|
||||
|
||||
if self.__active:
|
||||
util.cli.execute("notify-send 'Consuming caffeine'")
|
||||
else:
|
||||
util.cli.execute("notify-send 'Out of coffee'")
|
||||
|
||||
def _suspend_screensaver(self):
|
||||
self.__xid = self.__get_i3bar_xid()
|
||||
if self.__xid is None:
|
||||
return False
|
||||
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
os.setsid()
|
||||
util.cli.execute("xdg-screensaver suspend {}".format(self.__xid))
|
||||
os._exit(0)
|
||||
else:
|
||||
os.waitpid(pid, 0)
|
||||
return True
|
||||
|
||||
def __resume_screensaver(self):
|
||||
success = True
|
||||
xprop_path = shutil.which("xprop")
|
||||
pids = [
|
||||
p.pid
|
||||
for p in psutil.process_iter()
|
||||
if p.cmdline() == [xprop_path, "-id", str(self.__xid), "-spy"]
|
||||
]
|
||||
for pid in pids:
|
||||
try:
|
||||
os.kill(pid, 9)
|
||||
except OSError:
|
||||
success = False
|
||||
return success
|
||||
|
||||
def state(self, _):
|
||||
if self.__active:
|
||||
return "activated"
|
||||
return "deactivated"
|
||||
|
||||
def __toggle(self, _):
|
||||
missing = self.__check_requirements()
|
||||
if missing:
|
||||
logging.warning("Could not run caffeine - missing %s!", ", ".join(missing))
|
||||
return
|
||||
|
||||
self.__active = not self.__active
|
||||
if self.__active:
|
||||
success = self._suspend_screensaver()
|
||||
else:
|
||||
success = self.__resume_screensaver()
|
||||
|
||||
if success:
|
||||
self.__notify()
|
||||
else:
|
||||
self.__active = not self.__active
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
156
bumblebee_status/modules/contrib/cmus.py
Normal file
156
bumblebee_status/modules/contrib/cmus.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays information about the current song in cmus.
|
||||
|
||||
Requires the following executable:
|
||||
* cmus-remote
|
||||
|
||||
Parameters:
|
||||
* cmus.format: Format string for the song information. Tag values can be put in curly brackets (i.e. {artist})
|
||||
|
||||
Additional tags:
|
||||
* {file} - full song file name
|
||||
* {file1} - song file name without path prefix
|
||||
if {file} = '/foo/bar.baz', then {file1} = 'bar.baz'
|
||||
* {file2} - song file name without path prefix and extension suffix
|
||||
if {file} = '/foo/bar.baz', then {file2} = 'bar'
|
||||
* cmus.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles cmus.prev, cmus.next, cmus.shuffle and cmus.repeat, and the main display with play/pause function cmus.main.
|
||||
* cmus.server: The address of the cmus server, either a UNIX socket or host[:port]. Connects to the local instance by default.
|
||||
* cmus.passwd: The password to use for the TCP/IP connection.
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import os
|
||||
import string
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self._layout = self.parameter(
|
||||
"layout", "cmus.prev cmus.main cmus.next cmus.shuffle cmus.repeat"
|
||||
)
|
||||
self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}")
|
||||
self._server = self.parameter("server", None)
|
||||
self._passwd = self.parameter("passwd", None)
|
||||
self._status = None
|
||||
self._shuffle = False
|
||||
self._repeat = False
|
||||
self._tags = defaultdict(lambda: "")
|
||||
|
||||
# Create widgets
|
||||
widget_map = {}
|
||||
for widget_name in self._layout.split():
|
||||
widget = self.add_widget(name=widget_name)
|
||||
self._cmd = "cmus-remote"
|
||||
if self._server is not None:
|
||||
self._cmd = "{cmd} --server {server}".format(
|
||||
cmd=self._cmd, server=self._server
|
||||
)
|
||||
if self._passwd is not None:
|
||||
self._cmd = "{cmd} --passwd {passwd}".format(
|
||||
cmd=self._cmd, passwd=self._passwd
|
||||
)
|
||||
|
||||
if widget_name == "cmus.prev":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "{cmd} -r".format(cmd=self._cmd),
|
||||
}
|
||||
elif widget_name == "cmus.main":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "{cmd} -u".format(cmd=self._cmd),
|
||||
}
|
||||
widget.full_text(self.description)
|
||||
elif widget_name == "cmus.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "{cmd} -n".format(cmd=self._cmd),
|
||||
}
|
||||
elif widget_name == "cmus.shuffle":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "{cmd} -S".format(cmd=self._cmd),
|
||||
}
|
||||
elif widget_name == "cmus.repeat":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "{cmd} -R".format(cmd=self._cmd),
|
||||
}
|
||||
else:
|
||||
raise KeyError(
|
||||
"The cmus module does not support a {widget_name!r} widget".format(
|
||||
widget_name=widget_name
|
||||
)
|
||||
)
|
||||
|
||||
# Register input callbacks
|
||||
for widget, callback_options in widget_map.items():
|
||||
core.input.register(widget, **callback_options)
|
||||
|
||||
def hidden(self):
|
||||
return self._status is None
|
||||
|
||||
@core.decorators.scrollable
|
||||
def description(self, widget):
|
||||
return string.Formatter().vformat(self._fmt, (), self._tags)
|
||||
|
||||
def update(self):
|
||||
self._load_song()
|
||||
|
||||
def state(self, widget):
|
||||
returns = {
|
||||
"cmus.shuffle": "shuffle-on" if self._shuffle else "shuffle-off",
|
||||
"cmus.repeat": "repeat-on" if self._repeat else "repeat-off",
|
||||
"cmus.prev": "prev",
|
||||
"cmus.next": "next",
|
||||
}
|
||||
return returns.get(widget.name, self._status)
|
||||
|
||||
def _eval_line(self, line):
|
||||
if line.startswith("file "):
|
||||
full_file = line[5:]
|
||||
file1 = os.path.basename(full_file)
|
||||
file2 = os.path.splitext(file1)[0]
|
||||
self._tags.update({"file": full_file})
|
||||
self._tags.update({"file1": file1})
|
||||
self._tags.update({"file2": file2})
|
||||
return
|
||||
name, key, value = (line.split(" ", 2) + [None, None])[:3]
|
||||
|
||||
if name == "status":
|
||||
self._status = key
|
||||
if name == "tag":
|
||||
self._tags.update({key: value})
|
||||
if name in ["duration", "position"]:
|
||||
self._tags.update({name: util.format.duration(int(key))})
|
||||
if name == "set" and key == "repeat":
|
||||
self._repeat = value == "true"
|
||||
if name == "set" and key == "shuffle":
|
||||
self._shuffle = value == "true"
|
||||
|
||||
def _load_song(self):
|
||||
info = ""
|
||||
try:
|
||||
info = util.cli.execute("{cmd} -Q".format(cmd=self._cmd))
|
||||
except RuntimeError:
|
||||
self._status = None
|
||||
|
||||
self._tags = defaultdict(lambda: "")
|
||||
for line in info.split("\n"):
|
||||
self._eval_line(line)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
154
bumblebee_status/modules/contrib/cpu2.py
Normal file
154
bumblebee_status/modules/contrib/cpu2.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
"""Multiwidget CPU module
|
||||
|
||||
Can display any combination of:
|
||||
|
||||
* max CPU frequency
|
||||
* total CPU load in percents (integer value)
|
||||
* per-core CPU load as graph - either mono or colored
|
||||
* CPU temperature (in Celsius degrees)
|
||||
* CPU fan speed
|
||||
|
||||
Requirements:
|
||||
|
||||
* the psutil Python module for the first three items from the list above
|
||||
* sensors executable for the rest
|
||||
|
||||
Parameters:
|
||||
* cpu2.layout: Space-separated list of widgets to add.
|
||||
Possible widgets are:
|
||||
|
||||
* cpu2.maxfreq
|
||||
* cpu2.cpuload
|
||||
* cpu2.coresload
|
||||
* cpu2.temp
|
||||
* cpu2.fanspeed
|
||||
* cpu2.colored: 1 for colored per core load graph, 0 for mono (default)
|
||||
* cpu2.temp_pattern: pattern to look for in the output of 'sensors -u';
|
||||
required if cpu2.temp widged is used
|
||||
* cpu2.fan_pattern: pattern to look for in the output of 'sensors -u';
|
||||
required if cpu2.fanspeed widged is used
|
||||
|
||||
Note: if you are getting 'n/a' for CPU temperature / fan speed, then you're
|
||||
lacking the aforementioned pattern settings or they have wrong values.
|
||||
|
||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import psutil
|
||||
|
||||
import core.module
|
||||
|
||||
import util.cli
|
||||
import util.graph
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__layout = self.parameter(
|
||||
"layout", "cpu2.maxfreq cpu2.cpuload cpu2.coresload cpu2.temp cpu2.fanspeed"
|
||||
)
|
||||
self.__widget_names = self.__layout.split()
|
||||
self.__colored = util.format.asbool(self.parameter("colored", False))
|
||||
for widget_name in self.__widget_names:
|
||||
if widget_name == "cpu2.maxfreq":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.maxfreq)
|
||||
widget.set("type", "freq")
|
||||
elif widget_name == "cpu2.cpuload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.cpuload)
|
||||
widget.set("type", "load")
|
||||
elif widget_name == "cpu2.coresload":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.coresload)
|
||||
widget.set("type", "loads")
|
||||
elif widget_name == "cpu2.temp":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.temp)
|
||||
widget.set("type", "temp")
|
||||
elif widget_name == "cpu2.fanspeed":
|
||||
widget = self.add_widget(name=widget_name, full_text=self.fanspeed)
|
||||
widget.set("type", "fan")
|
||||
if self.__colored:
|
||||
widget.set("pango", True)
|
||||
self.__temp_pattern = self.parameter("temp_pattern")
|
||||
if self.__temp_pattern is None:
|
||||
self.__temp = "n/a"
|
||||
self.__fan_pattern = self.parameter("fan_pattern")
|
||||
if self.__fan_pattern is None:
|
||||
self.__fan = "n/a"
|
||||
# maxfreq is loaded only once at startup
|
||||
if "cpu2.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
|
||||
def maxfreq(self, _):
|
||||
return "{:.2f}GHz".format(self.__maxfreq)
|
||||
|
||||
def cpuload(self, _):
|
||||
return "{:>3}%".format(self.__cpuload)
|
||||
|
||||
def add_color(self, bar):
|
||||
"""add color as pango markup to a bar"""
|
||||
if bar in ["▁", "▂"]:
|
||||
color = self.theme.color("green", "green")
|
||||
elif bar in ["▃", "▄"]:
|
||||
color = self.theme.color("yellow", "yellow")
|
||||
elif bar in ["▅", "▆"]:
|
||||
color = self.theme.color("orange", "orange")
|
||||
elif bar in ["▇", "█"]:
|
||||
color = self.theme.color("red", "red")
|
||||
colored_bar = '<span foreground="{}">{}</span>'.format(color, bar)
|
||||
return colored_bar
|
||||
|
||||
def coresload(self, _):
|
||||
mono_bars = [util.graph.hbar(x) for x in self.__coresload]
|
||||
if not self.__colored:
|
||||
return "".join(mono_bars)
|
||||
colored_bars = [self.add_color(x) for x in mono_bars]
|
||||
return "".join(colored_bars)
|
||||
|
||||
def temp(self, _):
|
||||
if self.__temp == "n/a" or self.__temp == 0:
|
||||
return "n/a"
|
||||
return "{}°C".format(self.__temp)
|
||||
|
||||
def fanspeed(self, _):
|
||||
if self.__fanspeed == "n/a":
|
||||
return "n/a"
|
||||
return "{}RPM".format(self.__fanspeed)
|
||||
|
||||
def _parse_sensors_output(self):
|
||||
output = util.cli.execute("sensors -u")
|
||||
lines = output.split("\n")
|
||||
temp = "n/a"
|
||||
fan = "n/a"
|
||||
temp_line = None
|
||||
fan_line = None
|
||||
for line in lines:
|
||||
if self.__temp_pattern is not None and self.__temp_pattern in line:
|
||||
temp_line = line
|
||||
if self.__fan_pattern is not None and self.__fan_pattern in line:
|
||||
fan_line = line
|
||||
if temp_line is not None and fan_line is not None:
|
||||
break
|
||||
if temp_line is not None:
|
||||
temp = round(float(temp_line.split(":")[1].strip()))
|
||||
if fan_line is not None:
|
||||
fan = int(fan_line.split(":")[1].strip()[:-4])
|
||||
return temp, fan
|
||||
|
||||
def update(self):
|
||||
if "cpu2.maxfreq" in self.__widget_names:
|
||||
self.__maxfreq = psutil.cpu_freq().max / 1000
|
||||
if "cpu2.cpuload" in self.__widget_names:
|
||||
self.__cpuload = round(psutil.cpu_percent(percpu=False))
|
||||
if "cpu2.coresload" in self.__widget_names:
|
||||
self.__coresload = psutil.cpu_percent(percpu=True)
|
||||
if "cpu2.temp" in self.__widget_names or "cpu2.fanspeed" in self.__widget_names:
|
||||
self.__temp, self.__fanspeed = self._parse_sensors_output()
|
||||
|
||||
def state(self, widget):
|
||||
"""for having per-widget icons"""
|
||||
return [widget.get("type", "")]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
372
bumblebee_status/modules/contrib/currency.py
Normal file
372
bumblebee_status/modules/contrib/currency.py
Normal file
|
@ -0,0 +1,372 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays currency exchange rates. Currently, displays currency between GBP and USD/EUR only.
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
|
||||
Parameters:
|
||||
* currency.interval: Interval in minutes between updates, default is 1.
|
||||
* currency.source: Source currency (ex. 'GBP', 'EUR'). Defaults to 'auto', which infers the local one from IP address.
|
||||
* currency.destination: Comma-separated list of destination currencies (defaults to 'USD,EUR')
|
||||
* currency.sourceformat: String format for source formatting; Defaults to '{}: {}' and has two variables,
|
||||
the base symbol and the rate list
|
||||
* currency.destinationdelimiter: Delimiter used for separating individual rates (defaults to '|')
|
||||
|
||||
Note: source and destination names right now must correspond to the names used by the API of https://markets.ft.com
|
||||
|
||||
contributed by `AntouanK <https://github.com/AntouanK>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
from babel.numbers import format_currency
|
||||
except ImportError:
|
||||
format_currency = None
|
||||
import json
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.format
|
||||
import util.location
|
||||
|
||||
SYMBOL = {"GBP": "£", "EUR": "€", "USD": "$", "JPY": "¥", "KRW": "₩"}
|
||||
DEFAULT_DEST = "USD,EUR,auto"
|
||||
DEFAULT_SRC = "GBP"
|
||||
|
||||
API_URL = "https://markets.ft.com/data/currencies/ajax/conversion?baseCurrency={}&comparison={}"
|
||||
|
||||
|
||||
def load_country_to_currency():
|
||||
return [
|
||||
{"country": "Afghanistan", "currency_code": "AFN"},
|
||||
{"country": "Albania", "currency_code": "ALL"},
|
||||
{"country": "Algeria", "currency_code": "DZD"},
|
||||
{"country": "American Samoa", "currency_code": "USD"},
|
||||
{"country": "Andorra", "currency_code": "EUR"},
|
||||
{"country": "Angola", "currency_code": "AOA"},
|
||||
{"country": "Anguilla", "currency_code": "XCD"},
|
||||
{"country": "Antarctica", "currency_code": "XCD"},
|
||||
{"country": "Antigua and Barbuda", "currency_code": "XCD"},
|
||||
{"country": "Argentina", "currency_code": "ARS"},
|
||||
{"country": "Armenia", "currency_code": "AMD"},
|
||||
{"country": "Aruba", "currency_code": "AWG"},
|
||||
{"country": "Australia", "currency_code": "AUD"},
|
||||
{"country": "Austria", "currency_code": "EUR"},
|
||||
{"country": "Azerbaijan", "currency_code": "AZN"},
|
||||
{"country": "Bahamas", "currency_code": "BSD"},
|
||||
{"country": "Bahrain", "currency_code": "BHD"},
|
||||
{"country": "Bangladesh", "currency_code": "BDT"},
|
||||
{"country": "Barbados", "currency_code": "BBD"},
|
||||
{"country": "Belarus", "currency_code": "BYR"},
|
||||
{"country": "Belgium", "currency_code": "EUR"},
|
||||
{"country": "Belize", "currency_code": "BZD"},
|
||||
{"country": "Benin", "currency_code": "XOF"},
|
||||
{"country": "Bermuda", "currency_code": "BMD"},
|
||||
{"country": "Bhutan", "currency_code": "BTN"},
|
||||
{"country": "Bolivia", "currency_code": "BOB"},
|
||||
{"country": "Bosnia and Herzegovina", "currency_code": "BAM"},
|
||||
{"country": "Botswana", "currency_code": "BWP"},
|
||||
{"country": "Bouvet Island", "currency_code": "NOK"},
|
||||
{"country": "Brazil", "currency_code": "BRL"},
|
||||
{"country": "British Indian Ocean Territory", "currency_code": "USD"},
|
||||
{"country": "Brunei", "currency_code": "BND"},
|
||||
{"country": "Bulgaria", "currency_code": "BGN"},
|
||||
{"country": "Burkina Faso", "currency_code": "XOF"},
|
||||
{"country": "Burundi", "currency_code": "BIF"},
|
||||
{"country": "Cambodia", "currency_code": "KHR"},
|
||||
{"country": "Cameroon", "currency_code": "XAF"},
|
||||
{"country": "Canada", "currency_code": "CAD"},
|
||||
{"country": "Cape Verde", "currency_code": "CVE"},
|
||||
{"country": "Cayman Islands", "currency_code": "KYD"},
|
||||
{"country": "Central African Republic", "currency_code": "XAF"},
|
||||
{"country": "Chad", "currency_code": "XAF"},
|
||||
{"country": "Chile", "currency_code": "CLP"},
|
||||
{"country": "China", "currency_code": "CNY"},
|
||||
{"country": "Christmas Island", "currency_code": "AUD"},
|
||||
{"country": "Cocos (Keeling) Islands", "currency_code": "AUD"},
|
||||
{"country": "Colombia", "currency_code": "COP"},
|
||||
{"country": "Comoros", "currency_code": "KMF"},
|
||||
{"country": "Congo", "currency_code": "XAF"},
|
||||
{"country": "Cook Islands", "currency_code": "NZD"},
|
||||
{"country": "Costa Rica", "currency_code": "CRC"},
|
||||
{"country": "Croatia", "currency_code": "HRK"},
|
||||
{"country": "Cuba", "currency_code": "CUP"},
|
||||
{"country": "Cyprus", "currency_code": "EUR"},
|
||||
{"country": "Czech Republic", "currency_code": "CZK"},
|
||||
{"country": "Denmark", "currency_code": "DKK"},
|
||||
{"country": "Djibouti", "currency_code": "DJF"},
|
||||
{"country": "Dominica", "currency_code": "XCD"},
|
||||
{"country": "Dominican Republic", "currency_code": "DOP"},
|
||||
{"country": "East Timor", "currency_code": "USD"},
|
||||
{"country": "Ecuador", "currency_code": "ECS"},
|
||||
{"country": "Egypt", "currency_code": "EGP"},
|
||||
{"country": "El Salvador", "currency_code": "SVC"},
|
||||
{"country": "England", "currency_code": "GBP"},
|
||||
{"country": "Equatorial Guinea", "currency_code": "XAF"},
|
||||
{"country": "Eritrea", "currency_code": "ERN"},
|
||||
{"country": "Estonia", "currency_code": "EUR"},
|
||||
{"country": "Ethiopia", "currency_code": "ETB"},
|
||||
{"country": "Falkland Islands", "currency_code": "FKP"},
|
||||
{"country": "Faroe Islands", "currency_code": "DKK"},
|
||||
{"country": "Fiji Islands", "currency_code": "FJD"},
|
||||
{"country": "Finland", "currency_code": "EUR"},
|
||||
{"country": "France", "currency_code": "EUR"},
|
||||
{"country": "French Guiana", "currency_code": "EUR"},
|
||||
{"country": "French Polynesia", "currency_code": "XPF"},
|
||||
{"country": "French Southern territories", "currency_code": "EUR"},
|
||||
{"country": "Gabon", "currency_code": "XAF"},
|
||||
{"country": "Gambia", "currency_code": "GMD"},
|
||||
{"country": "Georgia", "currency_code": "GEL"},
|
||||
{"country": "Germany", "currency_code": "EUR"},
|
||||
{"country": "Ghana", "currency_code": "GHS"},
|
||||
{"country": "Gibraltar", "currency_code": "GIP"},
|
||||
{"country": "Greece", "currency_code": "EUR"},
|
||||
{"country": "Greenland", "currency_code": "DKK"},
|
||||
{"country": "Grenada", "currency_code": "XCD"},
|
||||
{"country": "Guadeloupe", "currency_code": "EUR"},
|
||||
{"country": "Guam", "currency_code": "USD"},
|
||||
{"country": "Guatemala", "currency_code": "QTQ"},
|
||||
{"country": "Guinea", "currency_code": "GNF"},
|
||||
{"country": "Guinea-Bissau", "currency_code": "CFA"},
|
||||
{"country": "Guyana", "currency_code": "GYD"},
|
||||
{"country": "Haiti", "currency_code": "HTG"},
|
||||
{"country": "Heard Island and McDonald Islands", "currency_code": "AUD"},
|
||||
{"country": "Holy See (Vatican City State)", "currency_code": "EUR"},
|
||||
{"country": "Honduras", "currency_code": "HNL"},
|
||||
{"country": "Hong Kong", "currency_code": "HKD"},
|
||||
{"country": "Hungary", "currency_code": "HUF"},
|
||||
{"country": "Iceland", "currency_code": "ISK"},
|
||||
{"country": "India", "currency_code": "INR"},
|
||||
{"country": "Indonesia", "currency_code": "IDR"},
|
||||
{"country": "Iran", "currency_code": "IRR"},
|
||||
{"country": "Iraq", "currency_code": "IQD"},
|
||||
{"country": "Ireland", "currency_code": "EUR"},
|
||||
{"country": "Israel", "currency_code": "ILS"},
|
||||
{"country": "Italy", "currency_code": "EUR"},
|
||||
{"country": "Ivory Coast", "currency_code": "XOF"},
|
||||
{"country": "Jamaica", "currency_code": "JMD"},
|
||||
{"country": "Japan", "currency_code": "JPY"},
|
||||
{"country": "Jordan", "currency_code": "JOD"},
|
||||
{"country": "Kazakhstan", "currency_code": "KZT"},
|
||||
{"country": "Kenya", "currency_code": "KES"},
|
||||
{"country": "Kiribati", "currency_code": "AUD"},
|
||||
{"country": "Kuwait", "currency_code": "KWD"},
|
||||
{"country": "Kyrgyzstan", "currency_code": "KGS"},
|
||||
{"country": "Laos", "currency_code": "LAK"},
|
||||
{"country": "Latvia", "currency_code": "LVL"},
|
||||
{"country": "Lebanon", "currency_code": "LBP"},
|
||||
{"country": "Lesotho", "currency_code": "LSL"},
|
||||
{"country": "Liberia", "currency_code": "LRD"},
|
||||
{"country": "Libyan Arab Jamahiriya", "currency_code": "LYD"},
|
||||
{"country": "Liechtenstein", "currency_code": "CHF"},
|
||||
{"country": "Lithuania", "currency_code": "LTL"},
|
||||
{"country": "Luxembourg", "currency_code": "EUR"},
|
||||
{"country": "Macao", "currency_code": "MOP"},
|
||||
{"country": "North Macedonia", "currency_code": "MKD"},
|
||||
{"country": "Madagascar", "currency_code": "MGF"},
|
||||
{"country": "Malawi", "currency_code": "MWK"},
|
||||
{"country": "Malaysia", "currency_code": "MYR"},
|
||||
{"country": "Maldives", "currency_code": "MVR"},
|
||||
{"country": "Mali", "currency_code": "XOF"},
|
||||
{"country": "Malta", "currency_code": "EUR"},
|
||||
{"country": "Marshall Islands", "currency_code": "USD"},
|
||||
{"country": "Martinique", "currency_code": "EUR"},
|
||||
{"country": "Mauritania", "currency_code": "MRO"},
|
||||
{"country": "Mauritius", "currency_code": "MUR"},
|
||||
{"country": "Mayotte", "currency_code": "EUR"},
|
||||
{"country": "Mexico", "currency_code": "MXN"},
|
||||
{"country": "Micronesia, Federated States of", "currency_code": "USD"},
|
||||
{"country": "Moldova", "currency_code": "MDL"},
|
||||
{"country": "Monaco", "currency_code": "EUR"},
|
||||
{"country": "Mongolia", "currency_code": "MNT"},
|
||||
{"country": "Montserrat", "currency_code": "XCD"},
|
||||
{"country": "Morocco", "currency_code": "MAD"},
|
||||
{"country": "Mozambique", "currency_code": "MZN"},
|
||||
{"country": "Myanmar", "currency_code": "MMR"},
|
||||
{"country": "Namibia", "currency_code": "NAD"},
|
||||
{"country": "Nauru", "currency_code": "AUD"},
|
||||
{"country": "Nepal", "currency_code": "NPR"},
|
||||
{"country": "Netherlands", "currency_code": "EUR"},
|
||||
{"country": "Netherlands Antilles", "currency_code": "ANG"},
|
||||
{"country": "New Caledonia", "currency_code": "XPF"},
|
||||
{"country": "New Zealand", "currency_code": "NZD"},
|
||||
{"country": "Nicaragua", "currency_code": "NIO"},
|
||||
{"country": "Niger", "currency_code": "XOF"},
|
||||
{"country": "Nigeria", "currency_code": "NGN"},
|
||||
{"country": "Niue", "currency_code": "NZD"},
|
||||
{"country": "Norfolk Island", "currency_code": "AUD"},
|
||||
{"country": "North Korea", "currency_code": "KPW"},
|
||||
{"country": "Northern Ireland", "currency_code": "GBP"},
|
||||
{"country": "Northern Mariana Islands", "currency_code": "USD"},
|
||||
{"country": "Norway", "currency_code": "NOK"},
|
||||
{"country": "Oman", "currency_code": "OMR"},
|
||||
{"country": "Pakistan", "currency_code": "PKR"},
|
||||
{"country": "Palau", "currency_code": "USD"},
|
||||
{"country": "Palestine", "currency_code": null},
|
||||
{"country": "Panama", "currency_code": "PAB"},
|
||||
{"country": "Papua New Guinea", "currency_code": "PGK"},
|
||||
{"country": "Paraguay", "currency_code": "PYG"},
|
||||
{"country": "Peru", "currency_code": "PEN"},
|
||||
{"country": "Philippines", "currency_code": "PHP"},
|
||||
{"country": "Pitcairn", "currency_code": "NZD"},
|
||||
{"country": "Poland", "currency_code": "PLN"},
|
||||
{"country": "Portugal", "currency_code": "EUR"},
|
||||
{"country": "Puerto Rico", "currency_code": "USD"},
|
||||
{"country": "Qatar", "currency_code": "QAR"},
|
||||
{"country": "Reunion", "currency_code": "EUR"},
|
||||
{"country": "Romania", "currency_code": "RON"},
|
||||
{"country": "Russian Federation", "currency_code": "RUB"},
|
||||
{"country": "Rwanda", "currency_code": "RWF"},
|
||||
{"country": "Saint Helena", "currency_code": "SHP"},
|
||||
{"country": "Saint Kitts and Nevis", "currency_code": "XCD"},
|
||||
{"country": "Saint Lucia", "currency_code": "XCD"},
|
||||
{"country": "Saint Pierre and Miquelon", "currency_code": "EUR"},
|
||||
{"country": "Saint Vincent and the Grenadines", "currency_code": "XCD"},
|
||||
{"country": "Samoa", "currency_code": "WST"},
|
||||
{"country": "San Marino", "currency_code": "EUR"},
|
||||
{"country": "Sao Tome and Principe", "currency_code": "STD"},
|
||||
{"country": "Saudi Arabia", "currency_code": "SAR"},
|
||||
{"country": "Scotland", "currency_code": "GBP"},
|
||||
{"country": "Senegal", "currency_code": "XOF"},
|
||||
{"country": "Seychelles", "currency_code": "SCR"},
|
||||
{"country": "Sierra Leone", "currency_code": "SLL"},
|
||||
{"country": "Singapore", "currency_code": "SGD"},
|
||||
{"country": "Slovakia", "currency_code": "EUR"},
|
||||
{"country": "Slovenia", "currency_code": "EUR"},
|
||||
{"country": "Solomon Islands", "currency_code": "SBD"},
|
||||
{"country": "Somalia", "currency_code": "SOS"},
|
||||
{"country": "South Africa", "currency_code": "ZAR"},
|
||||
{
|
||||
"country": "South Georgia and the South Sandwich Islands",
|
||||
"currency_code": "GBP",
|
||||
},
|
||||
{"country": "South Korea", "currency_code": "KRW"},
|
||||
{"country": "South Sudan", "currency_code": "SSP"},
|
||||
{"country": "Spain", "currency_code": "EUR"},
|
||||
{"country": "SriLanka", "currency_code": "LKR"},
|
||||
{"country": "Sudan", "currency_code": "SDG"},
|
||||
{"country": "Suriname", "currency_code": "SRD"},
|
||||
{"country": "Svalbard and Jan Mayen", "currency_code": "NOK"},
|
||||
{"country": "Swaziland", "currency_code": "SZL"},
|
||||
{"country": "Sweden", "currency_code": "SEK"},
|
||||
{"country": "Switzerland", "currency_code": "CHF"},
|
||||
{"country": "Syria", "currency_code": "SYP"},
|
||||
{"country": "Tajikistan", "currency_code": "TJS"},
|
||||
{"country": "Tanzania", "currency_code": "TZS"},
|
||||
{"country": "Thailand", "currency_code": "THB"},
|
||||
{"country": "The Democratic Republic of Congo", "currency_code": "CDF"},
|
||||
{"country": "Togo", "currency_code": "XOF"},
|
||||
{"country": "Tokelau", "currency_code": "NZD"},
|
||||
{"country": "Tonga", "currency_code": "TOP"},
|
||||
{"country": "Trinidad and Tobago", "currency_code": "TTD"},
|
||||
{"country": "Tunisia", "currency_code": "TND"},
|
||||
{"country": "Turkey", "currency_code": "TRY"},
|
||||
{"country": "Turkmenistan", "currency_code": "TMT"},
|
||||
{"country": "Turks and Caicos Islands", "currency_code": "USD"},
|
||||
{"country": "Tuvalu", "currency_code": "AUD"},
|
||||
{"country": "Uganda", "currency_code": "UGX"},
|
||||
{"country": "Ukraine", "currency_code": "UAH"},
|
||||
{"country": "United Arab Emirates", "currency_code": "AED"},
|
||||
{"country": "United Kingdom", "currency_code": "GBP"},
|
||||
{"country": "United States", "currency_code": "USD"},
|
||||
{"country": "United States Minor Outlying Islands", "currency_code": "USD"},
|
||||
{"country": "Uruguay", "currency_code": "UYU"},
|
||||
{"country": "Uzbekistan", "currency_code": "UZS"},
|
||||
{"country": "Vanuatu", "currency_code": "VUV"},
|
||||
{"country": "Venezuela", "currency_code": "VEF"},
|
||||
{"country": "Vietnam", "currency_code": "VND"},
|
||||
{"country": "Virgin Islands, British", "currency_code": "USD"},
|
||||
{"country": "Virgin Islands, U.S.", "currency_code": "USD"},
|
||||
{"country": "Wales", "currency_code": "GBP"},
|
||||
{"country": "Wallis and Futuna", "currency_code": "XPF"},
|
||||
{"country": "Western Sahara", "currency_code": "MAD"},
|
||||
{"country": "Yemen", "currency_code": "YER"},
|
||||
{"country": "Yugoslavia", "currency_code": null},
|
||||
{"country": "Zambia", "currency_code": "ZMW"},
|
||||
{"country": "Zimbabwe", "currency_code": "ZWD"},
|
||||
]
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.price))
|
||||
|
||||
self.__data = []
|
||||
|
||||
src = self.parameter("source", DEFAULT_SRC)
|
||||
if src == "auto":
|
||||
self.__base = self.find_local_currency()
|
||||
else:
|
||||
self.__base = src
|
||||
|
||||
self.__symbols = []
|
||||
for d in util.format.aslist(self.parameter("destination", DEFAULT_DEST)):
|
||||
if d == "auto":
|
||||
new = self.find_local_currency()
|
||||
else:
|
||||
new = d
|
||||
if new != self.__base:
|
||||
self.__symbols.append(new)
|
||||
|
||||
def price(self, widget):
|
||||
if len(self.__data) == 0:
|
||||
return "?"
|
||||
|
||||
rates = []
|
||||
for sym, rate in self.__data:
|
||||
rate_float = float(rate.replace(",", ""))
|
||||
if format_currency:
|
||||
rates.append(format_currency(rate_float, sym))
|
||||
else:
|
||||
rate = self.fmt_rate(rate)
|
||||
rates.append("{}{}".format(rate, SYMBOL[sym] if sym in SYMBOL else sym))
|
||||
|
||||
basefmt = "{}".format(self.parameter("sourceformat", "{}={}"))
|
||||
ratefmt = "{}".format(self.parameter("destinationdelimiter", "="))
|
||||
|
||||
if format_currency:
|
||||
base_val = format_currency(1, self.__base)
|
||||
else:
|
||||
base_val = "1{}".format(
|
||||
SYMBOL[self.__base] if self.__base in SYMBOL else self.__base
|
||||
)
|
||||
|
||||
return basefmt.format(base_val, ratefmt.join(rates))
|
||||
|
||||
def update(self):
|
||||
self.__data = []
|
||||
for symbol in self.__symbols:
|
||||
url = API_URL.format(self.__base, symbol)
|
||||
try:
|
||||
response = requests.get(url).json()
|
||||
self.__data.append((symbol, response["data"]["exchangeRate"]))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def find_local_currency(self):
|
||||
"""Use geolocation lookup to find local currency"""
|
||||
try:
|
||||
country = util.location.country()
|
||||
currency_map = load_country_to_currency()
|
||||
return currency_map.get(country, DEFAULT_SRC)
|
||||
except:
|
||||
return DEFAULT_SRC
|
||||
|
||||
def fmt_rate(self, rate):
|
||||
float_rate = float(rate.replace(",", ""))
|
||||
if not 0.01 < float_rate < 100:
|
||||
ret = rate
|
||||
else:
|
||||
ret = "%.3g" % float_rate
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
110
bumblebee_status/modules/contrib/datetimetz.py
Normal file
110
bumblebee_status/modules/contrib/datetimetz.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current date and time with timezone options.
|
||||
|
||||
Parameters:
|
||||
* datetimetz.format : strftime()-compatible formatting string
|
||||
* datetimetz.timezone : IANA timezone name
|
||||
* datetz.format : alias for datetimetz.format
|
||||
* timetz.format : alias for datetimetz.format
|
||||
* timetz.timezone : alias for datetimetz.timezone
|
||||
* datetimetz.locale : locale to use rather than the system default
|
||||
* datetz.locale : alias for datetimetz.locale
|
||||
* timetz.locale : alias for datetimetz.locale
|
||||
* timetz.timezone : alias for datetimetz.timezone
|
||||
|
||||
contributed by `frankzhao <https://github.com/frankzhao>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import datetime
|
||||
import locale
|
||||
import logging
|
||||
import pytz
|
||||
import tzlocal
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
def default_format(module):
|
||||
default = "%x %X %Z"
|
||||
if module == "datetz":
|
||||
default = "%x %Z"
|
||||
if module == "timetz":
|
||||
default = "%X %Z"
|
||||
return default
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.get_time))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.next_tz)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.prev_tz)
|
||||
self.__fmt = self.parameter("format", self.default_format())
|
||||
|
||||
default_timezone = ""
|
||||
try:
|
||||
default_timezone = tzlocal.get_localzone().zone
|
||||
except Exception as e:
|
||||
logging.error("unable to get default timezone: {}".format(str(e)))
|
||||
try:
|
||||
self._timezones = util.format.aslist(
|
||||
self.parameter("timezone", default_timezone)
|
||||
)
|
||||
except:
|
||||
self._timezones = [default_timezone]
|
||||
self._current_tz = 0
|
||||
|
||||
l = locale.getdefaultlocale()
|
||||
if not l or l == (None, None):
|
||||
l = ("en_US", "UTF-8")
|
||||
lcl = self.parameter("locale", ".".join(l))
|
||||
try:
|
||||
locale.setlocale(locale.LC_TIME, lcl.split("."))
|
||||
except Exception:
|
||||
locale.setlocale(locale.LC_TIME, ("en_US", "UTF-8"))
|
||||
|
||||
def default_format(self):
|
||||
return "%x %X %Z"
|
||||
|
||||
def get_time(self, widget):
|
||||
try:
|
||||
try:
|
||||
tz = pytz.timezone(self._timezones[self._current_tz].strip())
|
||||
retval = (
|
||||
datetime.datetime.now(tz=tzlocal.get_localzone())
|
||||
.astimezone(tz)
|
||||
.strftime(self.__fmt)
|
||||
)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
retval = "[Unknown timezone: {}]".format(
|
||||
self._timezones[self._current_tz].strip()
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error("unable to get time: {}".format(str(e)))
|
||||
retval = "[n/a]"
|
||||
|
||||
enc = locale.getpreferredencoding()
|
||||
if hasattr(retval, "decode"):
|
||||
return retval.decode(enc)
|
||||
return retval
|
||||
|
||||
def next_tz(self, event):
|
||||
next_timezone = self._current_tz + 1
|
||||
if next_timezone >= len(self._timezones):
|
||||
next_timezone = 0 # wraparound
|
||||
self._current_tz = next_timezone
|
||||
|
||||
def prev_tz(self, event):
|
||||
previous_timezone = self._current_tz - 1
|
||||
if previous_timezone < 0:
|
||||
previous_timezone = 0 # wraparound
|
||||
self._current_tz = previous_timezone
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
23
bumblebee_status/modules/contrib/datetz.py
Normal file
23
bumblebee_status/modules/contrib/datetz.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current date and time.
|
||||
|
||||
Parameters:
|
||||
* date.format: strftime()-compatible formatting string
|
||||
* date.locale: locale to use rather than the system default
|
||||
"""
|
||||
|
||||
import core.decorators
|
||||
from .datetimetz import Module
|
||||
|
||||
|
||||
class Module(Module):
|
||||
@core.decorators.every(hours=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme)
|
||||
|
||||
def default_format(self):
|
||||
return "%x %Z"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
149
bumblebee_status/modules/contrib/deadbeef.py
Normal file
149
bumblebee_status/modules/contrib/deadbeef.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current song being played in DeaDBeeF and provides
|
||||
some media control bindings.
|
||||
Left click toggles pause, scroll up skips the current song, scroll
|
||||
down returns to the previous song.
|
||||
|
||||
Requires the following library:
|
||||
* subprocess
|
||||
Parameters:
|
||||
* deadbeef.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {artist}, {title}, {album}, {length},
|
||||
{trackno}, {year}, {comment},
|
||||
{copyright}, {time}
|
||||
This is deprecated, but much simpler.
|
||||
* deadbeef.tf_format: A foobar2000 title formatting-style format string.
|
||||
These can be much more sophisticated than the standard
|
||||
format strings. This is off by default, but specifying
|
||||
any tf_format will enable it. If both deadbeef.format
|
||||
and deadbeef.tf_format are specified, deadbeef.tf_format
|
||||
takes priority.
|
||||
* deadbeef.tf_format_if_stopped: Controls whether or not the tf_format format
|
||||
string should be displayed even if no song is paused or
|
||||
playing. This could be useful if you want to implement
|
||||
your own stop strings with the built in logic. Any non-
|
||||
null value will enable this (by default the module will
|
||||
hide itself when the player is stopped).
|
||||
* deadbeef.previous: Change binding for previous song (default is left click)
|
||||
* deadbeef.next: Change binding for next song (default is right click)
|
||||
* deadbeef.pause: Change binding for toggling pause (default is middle click)
|
||||
|
||||
Available options for deadbeef.previous, deadbeef.next and deadbeef.pause are:
|
||||
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
|
||||
|
||||
contributed by `joshbarrass <https://github.com/joshbarrass>`_ - many thanks!
|
||||
"""
|
||||
import sys
|
||||
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.deadbeef))
|
||||
|
||||
buttons = {
|
||||
"LEFT_CLICK": core.input.LEFT_MOUSE,
|
||||
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
|
||||
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
|
||||
"SCROLL_UP": core.input.WHEEL_UP,
|
||||
"SCROLL_DOWN": core.input.WHEEL_DOWN,
|
||||
}
|
||||
|
||||
self._song = ""
|
||||
self._format = self.parameter("format", "{artist} - {title}")
|
||||
self._tf_format = self.parameter("tf_format", "")
|
||||
self._show_tf_when_stopped = util.format.asbool(
|
||||
self.parameter("tf_format_if_stopped", False)
|
||||
)
|
||||
prev_button = self.parameter("previous", "LEFT_CLICK")
|
||||
next_button = self.parameter("next", "RIGHT_CLICK")
|
||||
pause_button = self.parameter("pause", "MIDDLE_CLICK")
|
||||
|
||||
self.now_playing = "deadbeef --nowplaying %a;%t;%b;%l;%n;%y;%c;%r;%e"
|
||||
self.now_playing_tf = "deadbeef --nowplaying-tf "
|
||||
cmd = "deadbeef "
|
||||
|
||||
core.input.register(self, button=buttons[prev_button], cmd=cmd + "--prev")
|
||||
core.input.register(self, button=buttons[next_button], cmd=cmd + "--next")
|
||||
core.input.register(
|
||||
self, button=buttons[pause_button], cmd=cmd + "--play-pause"
|
||||
)
|
||||
|
||||
# modify the tf_format if we don't want it to show on stop
|
||||
# this adds conditions to the query itself, rather than
|
||||
# polling to see if deadbeef is running
|
||||
# doing this reduces the number of calls we have to make
|
||||
if self._tf_format and not self._show_tf_when_stopped:
|
||||
self._tf_format = "$if($or(%isplaying%,%ispaused%),{query})".format(
|
||||
query=self._tf_format
|
||||
)
|
||||
|
||||
@core.decorators.scrollable
|
||||
def deadbeef(self, widget):
|
||||
return self.string_song
|
||||
|
||||
def hidden(self):
|
||||
return self.string_song == ""
|
||||
|
||||
def update(self):
|
||||
widgets = self.widgets()
|
||||
try:
|
||||
if self._tf_format == "": # no tf format set, use the old style
|
||||
return self.update_standard(widgets)
|
||||
return self.update_tf(widgets)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self._song = "error"
|
||||
|
||||
def update_tf(self, widgets):
|
||||
## ensure that deadbeef is actually running
|
||||
## easiest way to do this is to check --nowplaying for
|
||||
## the string 'nothing'
|
||||
if util.cli.execute(self.now_playing) == "nothing":
|
||||
self._song = ""
|
||||
return
|
||||
## perform the actual query -- these can be much more sophisticated
|
||||
data = util.cli.execute(self.now_playing_tf + self._tf_format)
|
||||
self._song = data
|
||||
|
||||
def update_standard(self, widgets):
|
||||
data = util.cli.execute(self.now_playing)
|
||||
if data == "nothing":
|
||||
self._song = ""
|
||||
else:
|
||||
data = data.split(";")
|
||||
self._song = self._format.format(
|
||||
artist=data[0],
|
||||
title=data[1],
|
||||
album=data[2],
|
||||
length=data[3],
|
||||
trackno=data[4],
|
||||
year=data[5],
|
||||
comment=data[6],
|
||||
copyright=data[7],
|
||||
time=data[8],
|
||||
)
|
||||
|
||||
@property
|
||||
def string_song(self):
|
||||
"""\
|
||||
Returns the current song as a string, either as a unicode() (Python <
|
||||
3) or a regular str() (Python >= 3)
|
||||
"""
|
||||
if sys.version_info.major < 3:
|
||||
return unicode(self._song)
|
||||
return str(self._song)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
84
bumblebee_status/modules/contrib/deezer.py
Normal file
84
bumblebee_status/modules/contrib/deezer.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current song being played
|
||||
|
||||
Requires the following library:
|
||||
* python-dbus
|
||||
|
||||
Parameters:
|
||||
* deezer.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus}
|
||||
* deezer.previous: Change binding for previous song (default is left click)
|
||||
* deezer.next: Change binding for next song (default is right click)
|
||||
* deezer.pause: Change binding for toggling pause (default is middle click)
|
||||
|
||||
Available options for deezer.previous, deezer.next and deezer.pause are:
|
||||
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
|
||||
|
||||
contributed by `wwmoraes <https://github.com/wwmoraes>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import dbus
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.deezer))
|
||||
|
||||
buttons = {
|
||||
"LEFT_CLICK": core.input.LEFT_MOUSE,
|
||||
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
|
||||
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
|
||||
"SCROLL_UP": core.input.WHEEL_UP,
|
||||
"SCROLL_DOWN": core.input.WHEEL_DOWN,
|
||||
}
|
||||
|
||||
self._song = ""
|
||||
self._format = self.parameter("format", "{artist} - {title}")
|
||||
prev_button = self.parameter("previous", "LEFT_CLICK")
|
||||
next_button = self.parameter("next", "RIGHT_CLICK")
|
||||
pause_button = self.parameter("pause", "MIDDLE_CLICK")
|
||||
|
||||
cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.deezer \
|
||||
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
||||
core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous")
|
||||
core.input.register(self, button=buttons[next_button], cmd=cmd + "Next")
|
||||
core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause")
|
||||
|
||||
def deezer(self, widget):
|
||||
return str(self._song)
|
||||
|
||||
def hidden(self):
|
||||
return str(self._song) == ""
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
bus = dbus.SessionBus()
|
||||
deezer = bus.get_object(
|
||||
"org.mpris.MediaPlayer2.deezer", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
deezer_iface = dbus.Interface(deezer, "org.freedesktop.DBus.Properties")
|
||||
props = deezer_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
|
||||
playback_status = str(
|
||||
deezer_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
||||
)
|
||||
self._song = self._format.format(
|
||||
album=str(props.get("xesam:album")),
|
||||
title=str(props.get("xesam:title")),
|
||||
artist=",".join(props.get("xesam:artist")),
|
||||
trackNumber=str(props.get("xesam:trackNumber")),
|
||||
playbackStatus="\u25B6"
|
||||
if playback_status == "Playing"
|
||||
else "\u258D\u258D"
|
||||
if playback_status == "Paused"
|
||||
else "",
|
||||
)
|
||||
except Exception:
|
||||
self._song = ""
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
83
bumblebee_status/modules/contrib/dnf.py
Normal file
83
bumblebee_status/modules/contrib/dnf.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays DNF package update information (<security>/<bugfixes>/<enhancements>/<other>)
|
||||
|
||||
Requires the following executable:
|
||||
* dnf
|
||||
|
||||
Parameters:
|
||||
* dnf.interval: Time in minutes between two consecutive update checks (defaults to 30 minutes)
|
||||
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import core.event
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
def get_dnf_info(widget):
|
||||
res = util.cli.execute("dnf updateinfo", ignore_errors=True)
|
||||
|
||||
security = 0
|
||||
bugfixes = 0
|
||||
enhancements = 0
|
||||
other = 0
|
||||
for line in res.split("\n"):
|
||||
if not line.startswith(" "):
|
||||
continue
|
||||
elif "ecurity" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
security += int(s)
|
||||
elif "ugfix" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
bugfixes += int(s)
|
||||
elif "hancement" in line:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
enhancements += int(s)
|
||||
else:
|
||||
for s in line.split():
|
||||
if s.isdigit():
|
||||
other += int(s)
|
||||
|
||||
widget.set("security", security)
|
||||
widget.set("bugfixes", bugfixes)
|
||||
widget.set("enhancements", enhancements)
|
||||
widget.set("other", other)
|
||||
|
||||
core.event.trigger("update", [widget.module.id], redraw_only=True)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||
|
||||
def updates(self, widget):
|
||||
result = []
|
||||
for t in ["security", "bugfixes", "enhancements", "other"]:
|
||||
result.append(str(widget.get(t, 0)))
|
||||
return "/".join(result)
|
||||
|
||||
def update(self):
|
||||
thread = threading.Thread(target=get_dnf_info, args=(self.widget(),))
|
||||
thread.start()
|
||||
|
||||
def state(self, widget):
|
||||
cnt = 0
|
||||
for t in ["security", "bugfixes", "enhancements", "other"]:
|
||||
cnt += widget.get(t, 0)
|
||||
if cnt == 0:
|
||||
return "good"
|
||||
if cnt > 50 or widget.get("security", 0) > 0:
|
||||
return "critical"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
48
bumblebee_status/modules/contrib/docker_ps.py
Normal file
48
bumblebee_status/modules/contrib/docker_ps.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Displays the number of docker containers running
|
||||
|
||||
Requires the following python packages:
|
||||
* docker
|
||||
|
||||
contributed by `jlopezzarza <https://github.com/jlopezzarza>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import docker
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.docker_info))
|
||||
self.__info = ""
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
if self.__info == "OK - 0":
|
||||
state.append("warning")
|
||||
elif self.__info in ["n/a", "off"]:
|
||||
state.append("critical")
|
||||
return state
|
||||
|
||||
def docker_info(self, widget):
|
||||
try:
|
||||
cli = docker.DockerClient(base_url="unix://var/run/docker.sock")
|
||||
cli.ping()
|
||||
self.__info = "OK - {}".format(
|
||||
len(cli.containers.list(filters={"status": "running"}))
|
||||
)
|
||||
except ConnectionError:
|
||||
self.__info = "off"
|
||||
except Exception:
|
||||
self.__info = "n/a"
|
||||
return self.__info
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
37
bumblebee_status/modules/contrib/dunst.py
Normal file
37
bumblebee_status/modules/contrib/dunst.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Toggle dunst notifications.
|
||||
|
||||
contributed by `eknoes <https://github.com/eknoes>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(""))
|
||||
self._paused = False
|
||||
# Make sure that dunst is currently not paused
|
||||
util.cli.execute("killall -s SIGUSR2 dunst", ignore_errors=True)
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status)
|
||||
|
||||
def toggle_status(self, event):
|
||||
self._paused = not self._paused
|
||||
|
||||
try:
|
||||
if self._paused:
|
||||
util.cli.execute("killall -s SIGUSR1 dunst")
|
||||
else:
|
||||
util.cli.execute("killall -s SIGUSR2 dunst")
|
||||
except:
|
||||
self._paused = not self._paused # toggling failed
|
||||
|
||||
def state(self, widget):
|
||||
if self._paused:
|
||||
return ["muted", "warning"]
|
||||
return ["unmuted"]
|
89
bumblebee_status/modules/contrib/getcrypto.py
Normal file
89
bumblebee_status/modules/contrib/getcrypto.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the price of a cryptocurrency.
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
|
||||
Parameters:
|
||||
* getcrypto.interval: Interval in seconds for updating the price, default is 120, less than that will probably get your IP banned.
|
||||
* getcrypto.getbtc: 0 for not getting price of BTC, 1 for getting it (default).
|
||||
* getcrypto.geteth: 0 for not getting price of ETH, 1 for getting it (default).
|
||||
* getcrypto.getltc: 0 for not getting price of LTC, 1 for getting it (default).
|
||||
* getcrypto.getcur: Set the currency to display the price in, usd is the default.
|
||||
|
||||
contributed by `Ryunaq <https://github.com/Ryunaq>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
import time
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
def getfromkrak(coin, currency):
|
||||
abbrev = {
|
||||
"Btc": ["xbt", "XXBTZ"],
|
||||
"Eth": ["eth", "XETHZ"],
|
||||
"Ltc": ["ltc", "XLTCZ"],
|
||||
}
|
||||
data = abbrev.get(coin, None)
|
||||
if not data:
|
||||
return
|
||||
epair = "{}{}".format(data[0], currency)
|
||||
tickname = "{}{}".format(data[1], currency.upper())
|
||||
try:
|
||||
krakenget = requests.get(
|
||||
"https://api.kraken.com/0/public/Ticker?pair=" + epair
|
||||
).json()
|
||||
except (RequestException, Exception):
|
||||
return "No connection"
|
||||
if not "result" in krakenget:
|
||||
return "No data"
|
||||
kethusdask = float(krakenget["result"][tickname]["a"][0])
|
||||
kethusdbid = float(krakenget["result"][tickname]["b"][0])
|
||||
return coin + ": " + str((kethusdask + kethusdbid) / 2)[0:6]
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.curprice))
|
||||
|
||||
self.__curprice = ""
|
||||
self.__getbtc = util.format.asbool(self.parameter("getbtc", True))
|
||||
self.__geteth = util.format.asbool(self.parameter("geteth", True))
|
||||
self.__getltc = util.format.asbool(self.parameter("getltc", True))
|
||||
self.__getcur = self.parameter("getcur", "usd")
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="xdg-open https://cryptowat.ch/"
|
||||
)
|
||||
|
||||
def curprice(self, widget):
|
||||
return self.__curprice
|
||||
|
||||
def update(self):
|
||||
currency = self.__getcur
|
||||
btcprice, ethprice, ltcprice = "", "", ""
|
||||
if self.__getbtc:
|
||||
btcprice = getfromkrak("Btc", currency)
|
||||
if self.__geteth:
|
||||
ethprice = getfromkrak("Eth", currency)
|
||||
if self.__getltc:
|
||||
ltcprice = getfromkrak("Ltc", currency)
|
||||
self.__curprice = (
|
||||
btcprice
|
||||
+ " " * (self.__getbtc * self.__geteth)
|
||||
+ ethprice
|
||||
+ " " * (self.__getltc * max(self.__getbtc, self.__geteth))
|
||||
+ ltcprice
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
73
bumblebee_status/modules/contrib/github.py
Normal file
73
bumblebee_status/modules/contrib/github.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the unread GitHub notifications for a GitHub user
|
||||
|
||||
Requires the following library:
|
||||
* requests
|
||||
|
||||
Parameters:
|
||||
* github.token: GitHub user access token, the token needs to have the 'notifications' scope.
|
||||
* github.interval: Interval in minutes between updates, default is 5.
|
||||
|
||||
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import requests
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import core.input
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.github))
|
||||
|
||||
self.__count = 0
|
||||
self.__requests = requests.Session()
|
||||
self.__requests.headers.update(
|
||||
{"Authorization": "token {}".format(self.parameter("token", ""))}
|
||||
)
|
||||
|
||||
cmd = "xdg-open"
|
||||
if not shutil.which(cmd):
|
||||
cmd = "x-www-browser"
|
||||
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd="{} https://github.com/notifications".format(cmd),
|
||||
)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
|
||||
|
||||
def github(self, _):
|
||||
return str(self.__count)
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__count = 0
|
||||
url = "https://api.github.com/notifications"
|
||||
while True:
|
||||
notifications = self.__requests.get(url)
|
||||
self.__count += len(
|
||||
list(
|
||||
filter(
|
||||
lambda notification: notification["unread"],
|
||||
notifications.json(),
|
||||
)
|
||||
)
|
||||
)
|
||||
next_link = notifications.links.get("next")
|
||||
if next_link is not None:
|
||||
url = next_link.get("url")
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception:
|
||||
self.__count = "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
60
bumblebee_status/modules/contrib/gpmdp.py
Normal file
60
bumblebee_status/modules/contrib/gpmdp.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays information about the current song in Google Play music player.
|
||||
|
||||
Requires the following executable:
|
||||
* gpmdp-remote
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
widgets = [
|
||||
core.widget.Widget(name="gpmdp.prev"),
|
||||
core.widget.Widget(name="gpmdp.main", full_text=self.description),
|
||||
core.widget.Widget(name="gpmdp.next"),
|
||||
]
|
||||
super().__init__(config, theme, widgets)
|
||||
|
||||
core.input.register(
|
||||
widgets[0], button=core.input.LEFT_MOUSE, cmd="playerctl previous"
|
||||
)
|
||||
core.input.register(
|
||||
widgets[1], button=core.input.LEFT_MOUSE, cmd="playerctl play-pause"
|
||||
)
|
||||
core.input.register(
|
||||
widgets[2], button=core.input.LEFT_MOUSE, cmd="playerctl next"
|
||||
)
|
||||
|
||||
self.__status = None
|
||||
self.__tags = None
|
||||
|
||||
def description(self, widget):
|
||||
return self.__tags if self.__tags else "n/a"
|
||||
|
||||
def update(self):
|
||||
self.__load_song()
|
||||
|
||||
def state(self, widget):
|
||||
if widget.name == "gpmdp.prev":
|
||||
return "prev"
|
||||
if widget.name == "gpmdp.next":
|
||||
return "next"
|
||||
return self.__status
|
||||
|
||||
def __load_song(self):
|
||||
info = util.cli.execute("gpmdp-remote current", ignore_errors=True)
|
||||
status = util.cli.execute("gpmdp-remote status", ignore_errors=True)
|
||||
self.__status = status.split("\n")[0].lower()
|
||||
self.__tags = info.split("\n")[0]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
95
bumblebee_status/modules/contrib/hddtemp.py
Normal file
95
bumblebee_status/modules/contrib/hddtemp.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Fetch hard drive temeperature data from a hddtemp daemon
|
||||
that runs on localhost and default port (7634)
|
||||
|
||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
HOST = "localhost"
|
||||
PORT = 7634
|
||||
|
||||
CHUNK_SIZE = 1024
|
||||
RECORD_SIZE = 5
|
||||
SEPARATOR = "|"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.hddtemps))
|
||||
self.__hddtemps = self.__get_hddtemps()
|
||||
|
||||
def hddtemps(self, _):
|
||||
return self.__hddtemps
|
||||
|
||||
def __fetch_data(self):
|
||||
"""fetch data from hddtemp service"""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
sock.connect((HOST, PORT))
|
||||
data = ""
|
||||
while True:
|
||||
chunk = sock.recv(CHUNK_SIZE)
|
||||
if chunk:
|
||||
data += str(chunk)
|
||||
else:
|
||||
break
|
||||
return data
|
||||
except (AttributeError, socket.error) as e:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def __get_parts(data):
|
||||
"""
|
||||
split data using | separator and remove first item
|
||||
(because the first item is empty)
|
||||
"""
|
||||
parts = data.split("|")[1:]
|
||||
return parts
|
||||
|
||||
@staticmethod
|
||||
def __partition_parts(parts):
|
||||
"""
|
||||
partition parts: one device record is five (5) items
|
||||
"""
|
||||
per_disk = [
|
||||
parts[i : i + RECORD_SIZE] for i in range(len(parts))[::RECORD_SIZE]
|
||||
]
|
||||
return per_disk
|
||||
|
||||
@staticmethod
|
||||
def __get_name_and_temp(device_record):
|
||||
"""
|
||||
get device name (without /dev part, to save space on bar)
|
||||
and temperature (in °C) as tuple
|
||||
"""
|
||||
device_name = device_record[0].split("/")[-1]
|
||||
device_temp = device_record[2]
|
||||
return (device_name, device_temp)
|
||||
|
||||
@staticmethod
|
||||
def __get_hddtemp(device_record):
|
||||
name, temp = device_record
|
||||
hddtemp = "{}+{}°C".format(name, temp)
|
||||
return hddtemp
|
||||
|
||||
def __get_hddtemps(self):
|
||||
data = self.__fetch_data()
|
||||
if data is None:
|
||||
return "n/a"
|
||||
parts = self.__get_parts(data)
|
||||
per_disk = self.__partition_parts(parts)
|
||||
names_and_temps = [self.__get_name_and_temp(x) for x in per_disk]
|
||||
hddtemps = [self.__get_hddtemp(x) for x in names_and_temps]
|
||||
return SEPARATOR.join(hddtemps)
|
||||
|
||||
def update(self):
|
||||
self.__hddtemps = self.__get_hddtemps()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
28
bumblebee_status/modules/contrib/hostname.py
Normal file
28
bumblebee_status/modules/contrib/hostname.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the system hostname.
|
||||
|
||||
contributed by `varkokonyi <https://github.com/varkokonyi>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import platform
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__hname = ""
|
||||
|
||||
def output(self, _):
|
||||
return self.__hname + " " + "\uf233"
|
||||
|
||||
def update(self):
|
||||
self.__hname = platform.node()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
70
bumblebee_status/modules/contrib/http_status.py
Normal file
70
bumblebee_status/modules/contrib/http_status.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Display HTTP status code
|
||||
|
||||
Parameters:
|
||||
* http__status.label: Prefix label (optional)
|
||||
* http__status.target: Target to retrieve the HTTP status from
|
||||
* http__status.expect: Expected HTTP status
|
||||
|
||||
contributed by `valkheim <https://github.com/valkheim>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from requests import head
|
||||
|
||||
import psutil
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
UNK = "UNK"
|
||||
|
||||
@core.decorators.every(seconds=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__label = self.parameter("label")
|
||||
self.__target = self.parameter("target")
|
||||
self.__expect = self.parameter("expect", "200")
|
||||
|
||||
def labelize(self, s):
|
||||
if self.__label is None:
|
||||
return s
|
||||
return "{}: {}".format(self.__label, s)
|
||||
|
||||
def getStatus(self):
|
||||
try:
|
||||
res = head(self.__target)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return self.UNK
|
||||
else:
|
||||
status = str(res.status_code)
|
||||
return status
|
||||
|
||||
def getOutput(self):
|
||||
if self.__status == self.__expect:
|
||||
return self.labelize(self.__status)
|
||||
else:
|
||||
reason = " != {}".format(self.__expect)
|
||||
return self.labelize("{}{}".format(self.__status, reason))
|
||||
|
||||
def output(self, widget):
|
||||
return self.__output
|
||||
|
||||
def update(self):
|
||||
self.__status = self.getStatus()
|
||||
self.__output = self.getOutput()
|
||||
|
||||
def state(self, widget):
|
||||
if self.__status == self.UNK:
|
||||
return "warning"
|
||||
if self.__status != self.__expect:
|
||||
return "critical"
|
||||
return self.__output
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
62
bumblebee_status/modules/contrib/indicator.py
Normal file
62
bumblebee_status/modules/contrib/indicator.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the indicator status, for numlock, scrolllock and capslock
|
||||
|
||||
Parameters:
|
||||
* indicator.include: Comma-separated list of interface prefixes to include (defaults to 'numlock,capslock')
|
||||
* indicator.signalstype: If you want the signali type color to be 'critical' or 'warning' (defaults to 'warning')
|
||||
|
||||
contributed by `freed00m <https://github.com/freed00m>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__include = tuple(
|
||||
filter(
|
||||
len, util.format.aslist(self.parameter("include", "NumLock,CapsLock"))
|
||||
)
|
||||
)
|
||||
self.__signalType = (
|
||||
self.parameter("signaltype")
|
||||
if not self.parameter("signaltype") is None
|
||||
else "warning"
|
||||
)
|
||||
|
||||
def update(self):
|
||||
status_line = ""
|
||||
for line in (
|
||||
util.cli.execute("xset q", ignore_errors=True).replace(" ", "").split("\n")
|
||||
):
|
||||
if "capslock" in line.lower():
|
||||
status_line = line
|
||||
break
|
||||
for indicator in self.__include:
|
||||
widget = self.widget(indicator)
|
||||
if not widget:
|
||||
widget = self.add_widget(name=indicator, full_text=indicator)
|
||||
|
||||
widget.set(
|
||||
"status",
|
||||
True
|
||||
if "{}:on".format(indicator.lower()) in status_line.lower()
|
||||
else False,
|
||||
)
|
||||
|
||||
def state(self, widget):
|
||||
states = []
|
||||
if widget.get("status", False):
|
||||
states.append(self.__signalType)
|
||||
else:
|
||||
states.append("normal")
|
||||
return states
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
24
bumblebee_status/modules/contrib/kernel.py
Normal file
24
bumblebee_status/modules/contrib/kernel.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Shows Linux kernel version information
|
||||
|
||||
contributed by `pierre87 <https://github.com/pierre87>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import platform
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.full_text))
|
||||
|
||||
def full_text(self, widgets):
|
||||
return platform.release()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
42
bumblebee_status/modules/contrib/layout-xkbswitch.py
Normal file
42
bumblebee_status/modules/contrib/layout-xkbswitch.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
"""Displays and changes the current keyboard layout
|
||||
|
||||
Requires the following executable:
|
||||
* xkb-switch
|
||||
|
||||
contributed by `somospocos <https://github.com/somospocos>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.current_layout))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
|
||||
self.__current_layout = self.__get_current_layout()
|
||||
|
||||
def current_layout(self, _):
|
||||
return self.__current_layout
|
||||
|
||||
def __next_keymap(self, event):
|
||||
util.cli.execute("xkb-switch -n", ignore_errors=True)
|
||||
|
||||
def __get_current_layout(self):
|
||||
try:
|
||||
res = util.cli.execute("xkb-switch")
|
||||
return res.split("\n")[0]
|
||||
except RuntimeError:
|
||||
return ["n/a"]
|
||||
|
||||
def update(self):
|
||||
self.__current_layout = self.__get_current_layout()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
78
bumblebee_status/modules/contrib/layout.py
Normal file
78
bumblebee_status/modules/contrib/layout.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays and changes the current keyboard layout
|
||||
|
||||
Requires the following executable:
|
||||
* setxkbmap
|
||||
|
||||
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.current_layout))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__next_keymap)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_keymap)
|
||||
|
||||
def __next_keymap(self, event):
|
||||
self._set_keymap(1)
|
||||
|
||||
def __prev_keymap(self, event):
|
||||
self._set_keymap(-1)
|
||||
|
||||
def _set_keymap(self, rotation):
|
||||
layouts = self.get_layouts()
|
||||
if len(layouts) == 1:
|
||||
return # nothing to do
|
||||
layouts = layouts[rotation:] + layouts[:rotation]
|
||||
|
||||
layout_list = []
|
||||
variant_list = []
|
||||
for l in layouts:
|
||||
tmp = l.split(":")
|
||||
layout_list.append(tmp[0])
|
||||
variant_list.append(tmp[1] if len(tmp) > 1 else "")
|
||||
|
||||
util.cli.execute(
|
||||
"setxkbmap -layout {} -variant {}".format(
|
||||
",".join(layout_list), ",".join(variant_list)
|
||||
),
|
||||
ignore_errors=True,
|
||||
)
|
||||
|
||||
def get_layouts(self):
|
||||
try:
|
||||
res = util.cli.execute("setxkbmap -query")
|
||||
except RuntimeError:
|
||||
return ["n/a"]
|
||||
layouts = []
|
||||
variants = []
|
||||
for line in res.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
if "layout" in line:
|
||||
layouts = line.split(":")[1].strip().split(",")
|
||||
if "variant" in line:
|
||||
variants = line.split(":")[1].strip().split(",")
|
||||
|
||||
result = []
|
||||
for idx, layout in enumerate(layouts):
|
||||
if len(variants) > idx and variants[idx]:
|
||||
layout = "{}:{}".format(layout, variants[idx])
|
||||
result.append(layout)
|
||||
return result if len(result) > 0 else ["n/a"]
|
||||
|
||||
def current_layout(self, widget):
|
||||
layouts = self.get_layouts()
|
||||
return layouts[0]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
32
bumblebee_status/modules/contrib/libvirtvms.py
Normal file
32
bumblebee_status/modules/contrib/libvirtvms.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""Displays count of running libvirt VMs.
|
||||
|
||||
Required the following python packages:
|
||||
* libvirt
|
||||
|
||||
contributed by `maxpivo <https://github.com/maxpivo>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import sys
|
||||
import libvirt
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=10)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.status))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="virt-manager")
|
||||
|
||||
def status(self, _):
|
||||
conn = libvirt.openReadOnly(None)
|
||||
if conn == None:
|
||||
return "Failed to open connection to the hypervisor"
|
||||
return "VMs %s" % (conn.numOfDomains())
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
59
bumblebee_status/modules/contrib/mocp.py
Normal file
59
bumblebee_status/modules/contrib/mocp.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Displays information about the current song in mocp. Left click toggles play/pause. Right click toggles shuffle.
|
||||
|
||||
Requires the following executable:
|
||||
* mocp
|
||||
|
||||
Parameters:
|
||||
* mocp.format: Format string for the song information. Replace string sequences with the actual information:
|
||||
|
||||
* %state State
|
||||
* %file File
|
||||
* %title Title, includes track, artist, song title and album
|
||||
* %artist Artist
|
||||
* %song SongTitle
|
||||
* %album Album
|
||||
* %tt TotalTime
|
||||
* %tl TimeLeft
|
||||
* %ts TotalSec
|
||||
* %ct CurrentTime
|
||||
* %cs CurrentSec
|
||||
* %b Bitrate
|
||||
* %r Sample rate
|
||||
|
||||
contributed by `chrugi <https://github.com/chrugi>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.description))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="mocp -G")
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd="mocp -t shuffle")
|
||||
self.__format = self.parameter("format", "%state %artist - %song | %ct/%tt")
|
||||
self.__running = False
|
||||
|
||||
def description(self, widget):
|
||||
return self.__info if self.__running == True else "Music On Console Player"
|
||||
|
||||
def update(self):
|
||||
self.__load_song()
|
||||
|
||||
def __load_song(self):
|
||||
try:
|
||||
self.__info = util.cli.execute("mocp -Q '{}'".format(self.__format)).strip()
|
||||
self.__running = True
|
||||
except RuntimeError:
|
||||
self.__running = False
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
209
bumblebee_status/modules/contrib/mpd.py
Normal file
209
bumblebee_status/modules/contrib/mpd.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Displays information about the current song in mpd.
|
||||
|
||||
Requires the following executable:
|
||||
* mpc
|
||||
|
||||
Parameters:
|
||||
* mpd.format: Format string for the song information.
|
||||
|
||||
Supported tags (see `man mpc` for additional information)
|
||||
|
||||
* {name}
|
||||
* {artist}
|
||||
* {album}
|
||||
* {albumartist}
|
||||
* {comment}
|
||||
* {composer}
|
||||
* {date}
|
||||
* {originaldate}
|
||||
* {disc}
|
||||
* {genre}
|
||||
* {performer}
|
||||
* {title}
|
||||
* {track}
|
||||
* {time}
|
||||
* {file}
|
||||
* {id}
|
||||
* {prio}
|
||||
* {mtime}
|
||||
* {mdate}
|
||||
|
||||
Additional tags:
|
||||
|
||||
* {position} - position of currently playing song
|
||||
not to be confused with %position% mpc tag
|
||||
* {duration} - duration of currently playing song
|
||||
* {file1} - song file name without path prefix
|
||||
if {file} = '/foo/bar.baz', then {file1} = 'bar.baz'
|
||||
* {file2} - song file name without path prefix and extension suffix
|
||||
if {file} = '/foo/bar.baz', then {file2} = 'bar'
|
||||
|
||||
* mpd.host: MPD host to connect to. (mpc behaviour by default)
|
||||
* mpd.layout: Space-separated list of widgets to add. Possible widgets are the buttons/toggles mpd.prev, mpd.next, mpd.shuffle and mpd.repeat, and the main display with play/pause function mpd.main.
|
||||
|
||||
contributed by `alrayyes <https://github.com/alrayyes>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import string
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self._layout = self.parameter(
|
||||
"layout", "mpd.prev mpd.main mpd.next mpd.shuffle mpd.repeat"
|
||||
)
|
||||
|
||||
self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}")
|
||||
self._status = None
|
||||
self._shuffle = False
|
||||
self._repeat = False
|
||||
self._tags = defaultdict(lambda: "")
|
||||
|
||||
if not self.parameter("host"):
|
||||
self._hostcmd = ""
|
||||
else:
|
||||
self._hostcmd = " -h " + self.parameter("host")
|
||||
|
||||
# Create widgets
|
||||
widget_map = {}
|
||||
for widget_name in self._layout.split():
|
||||
widget = self.add_widget(name=widget_name)
|
||||
|
||||
if widget_name == "mpd.prev":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc prev" + self._hostcmd,
|
||||
}
|
||||
elif widget_name == "mpd.main":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc toggle" + self._hostcmd,
|
||||
}
|
||||
widget.full_text(self.description)
|
||||
elif widget_name == "mpd.next":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc next" + self._hostcmd,
|
||||
}
|
||||
elif widget_name == "mpd.shuffle":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc random" + self._hostcmd,
|
||||
}
|
||||
elif widget_name == "mpd.repeat":
|
||||
widget_map[widget] = {
|
||||
"button": core.input.LEFT_MOUSE,
|
||||
"cmd": "mpc repeat" + self._hostcmd,
|
||||
}
|
||||
else:
|
||||
raise KeyError(
|
||||
"The mpd module does not support a {widget_name!r} widget".format(
|
||||
widget_name=widget_name
|
||||
)
|
||||
)
|
||||
|
||||
# Register input callbacks
|
||||
for widget, callback_options in widget_map.items():
|
||||
core.input.register(widget, **callback_options)
|
||||
|
||||
def hidden(self):
|
||||
return self._status is None
|
||||
|
||||
@core.decorators.scrollable
|
||||
def description(self, widget):
|
||||
return string.Formatter().vformat(self._fmt, (), self._tags)
|
||||
|
||||
def update(self):
|
||||
self._load_song()
|
||||
|
||||
def state(self, widget):
|
||||
if widget.name == "mpd.shuffle":
|
||||
return "shuffle-on" if self._shuffle else "shuffle-off"
|
||||
if widget.name == "mpd.repeat":
|
||||
return "repeat-on" if self._repeat else "repeat-off"
|
||||
if widget.name == "mpd.prev":
|
||||
return "prev"
|
||||
if widget.name == "mpd.next":
|
||||
return "next"
|
||||
return self._status
|
||||
|
||||
def _load_song(self):
|
||||
info = ""
|
||||
tags = [
|
||||
"name",
|
||||
"artist",
|
||||
"album",
|
||||
"albumartist",
|
||||
"comment",
|
||||
"composer",
|
||||
"date",
|
||||
"originaldate",
|
||||
"disc",
|
||||
"genre",
|
||||
"performer",
|
||||
"title",
|
||||
"track",
|
||||
"time",
|
||||
"file",
|
||||
"id",
|
||||
"prio",
|
||||
"mtime",
|
||||
"mdate",
|
||||
]
|
||||
joinedtags = "\n".join(["tag {0} %{0}%".format(tag) for tag in tags])
|
||||
info = util.cli.execute(
|
||||
'mpc -f "{}"{}'.format(joinedtags, self._hostcmd), ignore_errors=True
|
||||
)
|
||||
|
||||
self._tags = defaultdict(lambda: "")
|
||||
self._status = None
|
||||
for line in info.split("\n"):
|
||||
if line.startswith("[playing]"):
|
||||
self._status = "playing"
|
||||
elif line.startswith("[paused]"):
|
||||
self._status = "paused"
|
||||
|
||||
if line.startswith("["):
|
||||
timer = line.split()[2]
|
||||
position = timer.split("/")[0]
|
||||
dur = timer.split("/")[1]
|
||||
duration = dur.split(" ")[0]
|
||||
self._tags.update({"position": position})
|
||||
self._tags.update({"duration": duration})
|
||||
|
||||
if line.startswith("volume"):
|
||||
value = line.split(" ", 2)[1:]
|
||||
for option in value:
|
||||
if option.startswith("repeat: on"):
|
||||
self._repeat = True
|
||||
elif option.startswith("repeat: off"):
|
||||
self._repeat = False
|
||||
elif option.startswith("random: on"):
|
||||
self._shuffle = True
|
||||
elif option.startswith("random: off"):
|
||||
self._shuffle = False
|
||||
if line.startswith("tag"):
|
||||
key, value = line.split(" ", 2)[1:]
|
||||
self._tags.update({key: value})
|
||||
if key == "file":
|
||||
self._tags.update({"file1": os.path.basename(value)})
|
||||
self._tags.update(
|
||||
{"file2": os.path.splitext(os.path.basename(value))[0]}
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
113
bumblebee_status/modules/contrib/network_traffic.py
Normal file
113
bumblebee_status/modules/contrib/network_traffic.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Displays network traffic
|
||||
* No extra configuration needed
|
||||
|
||||
contributed by `izn <https://github.com/izn>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import psutil
|
||||
import netifaces
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.format
|
||||
|
||||
WIDGET_NAME = "network_traffic"
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
widgets = [
|
||||
core.widget.Widget(
|
||||
module=self,
|
||||
name="{0}.rx".format(WIDGET_NAME),
|
||||
full_text=self.download_rate,
|
||||
),
|
||||
core.widget.Widget(
|
||||
module=self,
|
||||
name="{0}.tx".format(WIDGET_NAME),
|
||||
full_text=self.upload_rate,
|
||||
),
|
||||
]
|
||||
super().__init__(config, theme, widgets)
|
||||
|
||||
self.widgets()[0].set("theme.minwidth", "0000000KiB/s")
|
||||
self.widgets()[1].set("theme.minwidth", "0000000KiB/s")
|
||||
|
||||
try:
|
||||
self._bandwidth = BandwidthInfo()
|
||||
|
||||
self._rate_recv = "?"
|
||||
self._rate_sent = "?"
|
||||
self._bytes_recv = self._bandwidth.bytes_recv()
|
||||
self._bytes_sent = self._bandwidth.bytes_sent()
|
||||
except Exception:
|
||||
""" We do not want do explode anything """
|
||||
pass
|
||||
|
||||
def state(self, widget):
|
||||
"""Return the widget state"""
|
||||
|
||||
if widget.name == "{}.rx".format(WIDGET_NAME):
|
||||
return "rx"
|
||||
elif widget.name == "{}.tx".format(WIDGET_NAME):
|
||||
return "tx"
|
||||
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
bytes_recv = self._bandwidth.bytes_recv()
|
||||
bytes_sent = self._bandwidth.bytes_sent()
|
||||
|
||||
self._rate_recv = bytes_recv - self._bytes_recv
|
||||
self._rate_sent = bytes_sent - self._bytes_sent
|
||||
|
||||
self._bytes_recv, self._bytes_sent = bytes_recv, bytes_sent
|
||||
except Exception:
|
||||
""" We do not want do explode anything """
|
||||
pass
|
||||
|
||||
def download_rate(self, _):
|
||||
return "{}/s".format(util.format.byte(self._rate_recv))
|
||||
|
||||
def upload_rate(self, _):
|
||||
return "{}/s".format(util.format.byte(self._rate_sent))
|
||||
|
||||
|
||||
class BandwidthInfo(object):
|
||||
"""Get received/sent bytes from network adapter"""
|
||||
|
||||
def bytes_recv(self):
|
||||
"""Return received bytes"""
|
||||
return self.bandwidth().bytes_recv
|
||||
|
||||
def bytes_sent(self):
|
||||
"""Return sent bytes"""
|
||||
return self.bandwidth().bytes_sent
|
||||
|
||||
def bandwidth(self):
|
||||
"""Return bandwidth information"""
|
||||
io_counters = self.io_counters()
|
||||
return io_counters[self.default_network_adapter()]
|
||||
|
||||
@classmethod
|
||||
def default_network_adapter(cls):
|
||||
"""Return default active network adapter"""
|
||||
gateway = netifaces.gateways()["default"]
|
||||
|
||||
if not gateway:
|
||||
raise "No default gateway found"
|
||||
|
||||
return gateway[netifaces.AF_INET][1]
|
||||
|
||||
@classmethod
|
||||
def io_counters(cls):
|
||||
"""Return IO counters"""
|
||||
return psutil.net_io_counters(pernic=True)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
51
bumblebee_status/modules/contrib/notmuch_count.py
Normal file
51
bumblebee_status/modules/contrib/notmuch_count.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the result of a notmuch count query
|
||||
default : unread emails which path do not contained 'Trash' (notmuch count 'tag:unread AND NOT path:/.*Trash.*/')
|
||||
|
||||
Parameters:
|
||||
* notmuch_count.query: notmuch count query to show result
|
||||
|
||||
Errors:
|
||||
if the notmuch query failed, the shown value is -1
|
||||
|
||||
Dependencies:
|
||||
notmuch (https://notmuchmail.org/)
|
||||
|
||||
contributed by `abdoulayeYATERA <https://github.com/abdoulayeYATERA>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__notmuch_count_query = self.parameter(
|
||||
"query", "tag:unread AND NOT path:/.*Trash.*/"
|
||||
)
|
||||
|
||||
def output(self, widget):
|
||||
return self.__notmuch_count
|
||||
|
||||
def state(self, widgets):
|
||||
if self.__notmuch_count == 0:
|
||||
return "empty"
|
||||
return "items"
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__notmuch_count = util.cli.execute(
|
||||
"notmuch count {}".format(self.__notmuch_count_query)
|
||||
).strip()
|
||||
except Exception:
|
||||
self.__notmuch_count = "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
82
bumblebee_status/modules/contrib/nvidiagpu.py
Normal file
82
bumblebee_status/modules/contrib/nvidiagpu.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Displays GPU name, temperature and memory usage.
|
||||
|
||||
Parameters:
|
||||
* nvidiagpu.format: Format string (defaults to '{name}: {temp}°C %{usedmem}/{totalmem} MiB')
|
||||
Available values are: {name} {temp} {mem_used} {mem_total} {fanspeed} {clock_gpu} {clock_mem}
|
||||
|
||||
Requires nvidia-smi
|
||||
|
||||
contributed by `RileyRedpath <https://github.com/RileyRedpath>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.utilization))
|
||||
|
||||
self.__utilization = "Not found: 0 0/0"
|
||||
|
||||
def utilization(self, widget):
|
||||
return self.__utilization
|
||||
|
||||
def hidden(self):
|
||||
return "not found" in self.__utilization
|
||||
|
||||
def update(self):
|
||||
sp = util.cli.execute("nvidia-smi -q", ignore_errors=True)
|
||||
|
||||
title = ""
|
||||
usedMem = ""
|
||||
totalMem = ""
|
||||
temp = ""
|
||||
name = "not found"
|
||||
clockMem = ""
|
||||
clockGpu = ""
|
||||
fanspeed = ""
|
||||
for item in sp.split("\n"):
|
||||
try:
|
||||
key, val = item.split(":")
|
||||
key, val = key.strip(), val.strip()
|
||||
if title == "Clocks":
|
||||
if key == "Graphics":
|
||||
clockGpu = val.split(" ")[0]
|
||||
elif key == "Memory":
|
||||
clockMem = val.split(" ")[0]
|
||||
if title == "FB Memory Usage":
|
||||
if key == "Total":
|
||||
totalMem = val.split(" ")[0]
|
||||
elif key == "Used":
|
||||
usedMem = val.split(" ")[0]
|
||||
elif key == "GPU Current Temp":
|
||||
temp = val.split(" ")[0]
|
||||
elif key == "Product Name":
|
||||
name = val
|
||||
elif key == "Fan Speed":
|
||||
fanspeed = val.split(" ")[0]
|
||||
|
||||
except:
|
||||
title = item.strip()
|
||||
|
||||
str_format = self.parameter(
|
||||
"format", "{name}: {temp}°C {mem_used}/{mem_total} MiB"
|
||||
)
|
||||
self.__utilization = str_format.format(
|
||||
name=name,
|
||||
temp=temp,
|
||||
mem_used=usedMem,
|
||||
mem_total=totalMem,
|
||||
clock_gpu=clockGpu,
|
||||
clock_mem=clockMem,
|
||||
fanspeed=fanspeed,
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
254
bumblebee_status/modules/contrib/octoprint.py
Normal file
254
bumblebee_status/modules/contrib/octoprint.py
Normal file
|
@ -0,0 +1,254 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the Octorpint status and the printer's bed/tools temperature in the status bar.
|
||||
|
||||
Left click opens a popup which shows the bed & tools temperatures and additionally a livestream of the webcam (if enabled).
|
||||
|
||||
Parameters:
|
||||
* octoprint.address : Octoprint address (e.q: http://192.168.1.3)
|
||||
* octoprint.apitoken : Octorpint API Token (can be obtained from the Octoprint Webinterface)
|
||||
* octoprint.webcam : Set to True if a webcam is connected (default: False)
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
|
||||
import urllib
|
||||
import logging
|
||||
import threading
|
||||
import queue
|
||||
|
||||
import tkinter as tk
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
import requests
|
||||
import simplejson
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
def get_frame(url):
|
||||
img_bytes = b""
|
||||
stream = urllib.request.urlopen(url)
|
||||
while True:
|
||||
img_bytes += stream.read(1024)
|
||||
a = img_bytes.find(b"\xff\xd8")
|
||||
b = img_bytes.find(b"\xff\xd9")
|
||||
if a != -1 and b != -1:
|
||||
jpg = img_bytes[a : b + 2]
|
||||
img_bytes = img_bytes[b + 2 :]
|
||||
img = Image.open(BytesIO(jpg))
|
||||
return img
|
||||
return None
|
||||
|
||||
|
||||
class WebcamImagesWorker(threading.Thread):
|
||||
def __init__(self, url, queue):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.__url = url
|
||||
self.__queue = queue
|
||||
self.__running = True
|
||||
|
||||
def run(self):
|
||||
while self.__running:
|
||||
img = get_frame(self.__url)
|
||||
self.__queue.put(img)
|
||||
|
||||
def stop(self):
|
||||
self.__running = False
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.octoprint_status))
|
||||
|
||||
self.__octoprint_state = "Unknown"
|
||||
self.__octoprint_address = self.parameter("address", "")
|
||||
self.__octoprint_api_token = self.parameter("apitoken", "")
|
||||
self.__octoprint_webcam = self.parameter("webcam", False)
|
||||
|
||||
self.__webcam_images_worker = None
|
||||
self.__webcam_image_url = self.__octoprint_address + "/webcam/?action=stream"
|
||||
self.__webcam_images_queue = None
|
||||
|
||||
self.__printer_bed_temperature = "-"
|
||||
self.__tool1_temperature = "-"
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__show_popup)
|
||||
|
||||
def octoprint_status(self, widget):
|
||||
if self.__octoprint_state == "Offline" or self.__octoprint_state == "Unknown":
|
||||
return self.__octoprint_state
|
||||
return (
|
||||
self.__octoprint_state
|
||||
+ " | B: "
|
||||
+ str(self.__printer_bed_temperature)
|
||||
+ "°C"
|
||||
+ " | T1: "
|
||||
+ str(self.__tool1_temperature)
|
||||
+ "°C"
|
||||
)
|
||||
|
||||
def __get(self, endpoint):
|
||||
url = self.__octoprint_address + "/api/" + endpoint
|
||||
headers = {"X-Api-Key": self.__octoprint_api_token}
|
||||
resp = requests.get(url, headers=headers)
|
||||
|
||||
try:
|
||||
return resp.json(), resp.status_code
|
||||
except simplejson.errors.JSONDecodeError:
|
||||
return None, resp.status_code
|
||||
|
||||
def __get_printer_bed_temperature(self):
|
||||
printer_info, status_code = self.__get("printer")
|
||||
if status_code == 200:
|
||||
return (
|
||||
printer_info["temperature"]["bed"]["actual"],
|
||||
printer_info["temperature"]["bed"]["target"],
|
||||
)
|
||||
return None, None
|
||||
|
||||
def __get_octoprint_state(self):
|
||||
job_info, status_code = self.__get("job")
|
||||
return job_info["state"] if status_code == 200 else "Unknown"
|
||||
|
||||
def __get_tool_temperatures(self):
|
||||
tool_temperatures = []
|
||||
|
||||
printer_info, status_code = self.__get("printer")
|
||||
if status_code == 200:
|
||||
temperatures = printer_info["temperature"]
|
||||
|
||||
tool_id = 0
|
||||
while True:
|
||||
try:
|
||||
tool = temperatures["tool" + str(tool_id)]
|
||||
tool_temperatures.append((tool["actual"], tool["target"]))
|
||||
except KeyError:
|
||||
break
|
||||
tool_id += 1
|
||||
return tool_temperatures
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__octoprint_state = self.__get_octoprint_state()
|
||||
|
||||
actual_temp, _ = self.__get_printer_bed_temperature()
|
||||
if actual_temp is None:
|
||||
actual_temp = "-"
|
||||
self.__printer_bed_temperature = str(actual_temp)
|
||||
|
||||
tool_temps = self.__get_tool_temperatures()
|
||||
if len(tool_temps) > 0:
|
||||
self.__tool1_temperature = tool_temps[0][0]
|
||||
else:
|
||||
self.__tool1_temperature = "-"
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't get data")
|
||||
|
||||
def __refresh_image(self, root, webcam_image, webcam_image_container):
|
||||
try:
|
||||
img = self.__webcam_images_queue.get()
|
||||
webcam_image = ImageTk.PhotoImage(img)
|
||||
webcam_image_container.config(image=webcam_image)
|
||||
except queue.Empty as e:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.exception("Couldn't refresh image")
|
||||
|
||||
root.after(5, self.__refresh_image, root, webcam_image, webcam_image_container)
|
||||
|
||||
def __refresh_temperatures(
|
||||
self, root, printer_bed_temperature_label, tools_temperature_label
|
||||
):
|
||||
actual_bed_temp, target_bed_temp = self.__get_printer_bed_temperature()
|
||||
if actual_bed_temp is None:
|
||||
actual_bed_temp = "-"
|
||||
if target_bed_temp is None:
|
||||
target_bed_temp = "-"
|
||||
|
||||
bed_temp = "Bed: " + str(actual_bed_temp) + "/" + str(target_bed_temp) + " °C"
|
||||
printer_bed_temperature_label.config(text=bed_temp)
|
||||
|
||||
tool_temperatures = self.__get_tool_temperatures()
|
||||
tools_temp = "Tools: "
|
||||
|
||||
if len(tool_temperatures) == 0:
|
||||
tools_temp += "-/- °C"
|
||||
else:
|
||||
for i, tool_temperature in enumerate(tool_temperatures):
|
||||
tools_temp += (
|
||||
str(tool_temperature[0]) + "/" + str(tool_temperature[1]) + "°C"
|
||||
)
|
||||
if i != len(tool_temperatures) - 1:
|
||||
tools_temp += "\t"
|
||||
tools_temperature_label.config(text=tools_temp)
|
||||
|
||||
root.after(
|
||||
500,
|
||||
self.__refresh_temperatures,
|
||||
root,
|
||||
printer_bed_temperature_label,
|
||||
tools_temperature_label,
|
||||
)
|
||||
|
||||
def __show_popup(self, widget):
|
||||
root = tk.Tk()
|
||||
root.attributes("-type", "dialog")
|
||||
root.title("Octoprint")
|
||||
frame = tk.Frame(root)
|
||||
if self.__octoprint_webcam:
|
||||
|
||||
# load first image synchronous before popup is shown, otherwise tkinter isn't able to layout popup properly
|
||||
img = get_frame(self.__webcam_image_url)
|
||||
webcam_image = ImageTk.PhotoImage(img)
|
||||
webcam_image_container = tk.Button(frame, image=webcam_image)
|
||||
webcam_image_container.pack()
|
||||
|
||||
self.__webcam_images_queue = queue.Queue()
|
||||
|
||||
self.__webcam_images_worker = WebcamImagesWorker(
|
||||
self.__webcam_image_url, self.__webcam_images_queue
|
||||
)
|
||||
self.__webcam_images_worker.start()
|
||||
else:
|
||||
logging.debug(
|
||||
"Not using webcam, as webcam is disabled. Enable with --webcam."
|
||||
)
|
||||
frame.pack()
|
||||
|
||||
temperatures_label = tk.Label(frame, text="Temperatures", font=("", 25))
|
||||
temperatures_label.pack()
|
||||
|
||||
printer_bed_temperature_label = tk.Label(
|
||||
frame, text="Bed: -/- °C", font=("", 15)
|
||||
)
|
||||
printer_bed_temperature_label.pack()
|
||||
|
||||
tools_temperature_label = tk.Label(frame, text="Tools: -/- °C", font=("", 15))
|
||||
tools_temperature_label.pack()
|
||||
|
||||
root.after(10, self.__refresh_image, root, webcam_image, webcam_image_container)
|
||||
root.after(
|
||||
500,
|
||||
self.__refresh_temperatures,
|
||||
root,
|
||||
printer_bed_temperature_label,
|
||||
tools_temperature_label,
|
||||
)
|
||||
root.bind("<Destroy>", self.__on_close_popup)
|
||||
|
||||
root.eval("tk::PlaceWindow . center")
|
||||
root.mainloop()
|
||||
|
||||
def __on_close_popup(self, event):
|
||||
self.__webcam_images_queue = None
|
||||
self.__webcam_images_worker.stop()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
78
bumblebee_status/modules/contrib/pacman.py
Normal file
78
bumblebee_status/modules/contrib/pacman.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays update information per repository for pacman.
|
||||
|
||||
Parameters:
|
||||
* pacman.sum: If you prefere displaying updates with a single digit (defaults to 'False')
|
||||
|
||||
Requires the following executables:
|
||||
* fakeroot
|
||||
* pacman
|
||||
|
||||
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
# list of repositories.
|
||||
# the last one should always be other
|
||||
repos = ["core", "extra", "community", "multilib", "testing", "other"]
|
||||
|
||||
|
||||
def get_pacman_info(widget, path):
|
||||
cmd = "{}/../../bin/pacman-updates".format(path)
|
||||
if not os.path.exists(cmd):
|
||||
cmd = "/usr/share/bumblebee-status/bin/pacman-update"
|
||||
result = util.cli.execute(cmd, ignore_errors=True)
|
||||
|
||||
count = len(repos) * [0]
|
||||
|
||||
for line in result.splitlines():
|
||||
if line.startswith(("http", "rsync")):
|
||||
for i in range(len(repos) - 1):
|
||||
if "/" + repos[i] + "/" in line:
|
||||
count[i] += 1
|
||||
break
|
||||
else:
|
||||
result[-1] += 1
|
||||
|
||||
for i in range(len(repos)):
|
||||
widget.set(repos[i], count[i])
|
||||
core.event.trigger("update", [widget.module.id], redraw_only=True)
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=30)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.updates))
|
||||
|
||||
def updates(self, widget):
|
||||
if util.format.asbool(self.parameter("sum")):
|
||||
return str(sum(map(lambda x: widget.get(x, 0), repos)))
|
||||
return "/".join(map(lambda x: str(widget.get(x, 0)), repos))
|
||||
|
||||
def update(self):
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
thread = threading.Thread(target=get_pacman_info, args=(self.widget(), path))
|
||||
thread.start()
|
||||
|
||||
def state(self, widget):
|
||||
weightedCount = sum(
|
||||
map(lambda x: (len(repos) - x[0]) * widget.get(x[1], 0), enumerate(repos))
|
||||
)
|
||||
|
||||
if weightedCount < 10:
|
||||
return "good"
|
||||
|
||||
return self.threshold_state(weightedCount, 100, 150)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
85
bumblebee_status/modules/contrib/pihole.py
Normal file
85
bumblebee_status/modules/contrib/pihole.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the pi-hole status (up/down) together with the number of ads that were blocked today
|
||||
|
||||
Parameters:
|
||||
* pihole.address : pi-hole address (e.q: http://192.168.1.3)
|
||||
* pihole.pwhash : pi-hole webinterface password hash (can be obtained from the /etc/pihole/SetupVars.conf file)
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.pihole_status))
|
||||
|
||||
self._pihole_address = self.parameter("address", "")
|
||||
self._pihole_pw_hash = self.parameter("pwhash", "")
|
||||
self._pihole_status = None
|
||||
self._ads_blocked_today = "-"
|
||||
self.update_pihole_status()
|
||||
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd=self.toggle_pihole_status
|
||||
)
|
||||
|
||||
def pihole_status(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return "pi-hole unknown"
|
||||
return "pi-hole {}".format(
|
||||
"up {} blocked".format(self._ads_blocked_today)
|
||||
if self._pihole_status
|
||||
else "down"
|
||||
)
|
||||
|
||||
def update_pihole_status(self):
|
||||
try:
|
||||
data = requests.get(self._pihole_address + "/admin/api.php?summary").json()
|
||||
self._pihole_status = True if data["status"] == "enabled" else False
|
||||
self._ads_blocked_today = data["ads_blocked_today"]
|
||||
except Exception as e:
|
||||
self._pihole_status = None
|
||||
|
||||
def toggle_pihole_status(self, widget):
|
||||
if self._pihole_status is not None:
|
||||
try:
|
||||
req = None
|
||||
if self._pihole_status:
|
||||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?disable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
)
|
||||
else:
|
||||
req = requests.get(
|
||||
self._pihole_address
|
||||
+ "/admin/api.php?enable&auth="
|
||||
+ self._pihole_pw_hash
|
||||
)
|
||||
if req is not None:
|
||||
if req.status_code == 200:
|
||||
status = req.json()["status"]
|
||||
self._pihole_status = False if status == "disabled" else True
|
||||
except:
|
||||
pass
|
||||
|
||||
def update(self):
|
||||
self.update_pihole_status()
|
||||
|
||||
def state(self, widget):
|
||||
if self._pihole_status is None:
|
||||
return []
|
||||
elif self._pihole_status:
|
||||
return ["enabled"]
|
||||
return ["disabled", "warning"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
129
bumblebee_status/modules/contrib/pomodoro.py
Normal file
129
bumblebee_status/modules/contrib/pomodoro.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Display and run a Pomodoro timer.
|
||||
Left click to start timer, left click again to pause.
|
||||
Right click will cancel the timer.
|
||||
|
||||
Parameters:
|
||||
* pomodoro.work: The work duration of timer in minutes (defaults to 25)
|
||||
* pomodoro.break: The break duration of timer in minutes (defaults to 5)
|
||||
* pomodoro.format: Timer display format with '%m' and '%s' for minutes and seconds (defaults to '%m:%s')
|
||||
Examples: '%m min %s sec', '%mm', '', 'timer'
|
||||
* pomodoro.notify: Notification command to run when timer ends/starts (defaults to nothing)
|
||||
Example: 'notify-send 'Time up!''. If you want to chain multiple commands,
|
||||
please use an external wrapper script and invoke that. The module itself does
|
||||
not support command chaining (see https://github.com/tobi-wan-kenobi/bumblebee-status/issues/532
|
||||
for a detailled explanation)
|
||||
|
||||
contributed by `martindoublem <https://github.com/martindoublem>`_, inspired by `karthink <https://github.com/karthink>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import datetime
|
||||
from math import ceil
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.text))
|
||||
|
||||
# Parameters
|
||||
self.__work_period = int(self.parameter("work", 25))
|
||||
self.__break_period = int(self.parameter("break", 5))
|
||||
self.__time_format = self.parameter("format", "%m:%s")
|
||||
self.__notify_cmd = self.parameter("notify", "")
|
||||
|
||||
# TODO: Handle time formats more gracefully. This is kludge.
|
||||
self.display_seconds_p = False
|
||||
self.display_minutes_p = False
|
||||
if "%s" in self.__time_format:
|
||||
self.display_seconds_p = True
|
||||
if "%m" in self.__time_format:
|
||||
self.display_minutes_p = True
|
||||
|
||||
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
|
||||
|
||||
self.time = None
|
||||
self.pomodoro = {"state": "OFF", "type": ""}
|
||||
self.__text = self.remaining_time_str() + self.pomodoro["type"]
|
||||
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd=self.timer_play_pause
|
||||
)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.timer_reset)
|
||||
|
||||
def remaining_time_str(self):
|
||||
if self.display_seconds_p and self.display_minutes_p:
|
||||
minutes, seconds = divmod(self.remaining_time.seconds, 60)
|
||||
if not self.display_seconds_p:
|
||||
minutes = ceil(self.remaining_time.seconds / 60)
|
||||
seconds = 0
|
||||
if not self.display_minutes_p:
|
||||
minutes = 0
|
||||
seconds = self.remaining_time.seconds
|
||||
|
||||
minutes = "{:2d}".format(minutes)
|
||||
seconds = "{:02d}".format(seconds)
|
||||
return self.__time_format.replace("%m", minutes).replace("%s", seconds) + " "
|
||||
|
||||
def text(self, widget):
|
||||
return "{}".format(self.__text)
|
||||
|
||||
def update(self):
|
||||
if self.pomodoro["state"] == "ON":
|
||||
timediff = datetime.datetime.now() - self.time
|
||||
if timediff.seconds >= 0:
|
||||
self.remaining_time -= timediff
|
||||
self.time = datetime.datetime.now()
|
||||
|
||||
if self.remaining_time.total_seconds() <= 0:
|
||||
self.notify()
|
||||
if self.pomodoro["type"] == "Work":
|
||||
self.pomodoro["type"] = "Break"
|
||||
self.remaining_time = datetime.timedelta(
|
||||
minutes=self.__break_period
|
||||
)
|
||||
elif self.pomodoro["type"] == "Break":
|
||||
self.pomodoro["type"] = "Work"
|
||||
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
|
||||
|
||||
self.__text = self.remaining_time_str() + self.pomodoro["type"]
|
||||
|
||||
def notify(self):
|
||||
if self.__notify_cmd:
|
||||
util.cli.execute(self.__notify_cmd)
|
||||
|
||||
def timer_play_pause(self, widget):
|
||||
if self.pomodoro["state"] == "OFF":
|
||||
self.pomodoro = {"state": "ON", "type": "Work"}
|
||||
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
|
||||
self.time = datetime.datetime.now()
|
||||
elif self.pomodoro["state"] == "ON":
|
||||
self.pomodoro["state"] = "PAUSED"
|
||||
self.remaining_time -= datetime.datetime.now() - self.time
|
||||
self.time = datetime.datetime.now()
|
||||
elif self.pomodoro["state"] == "PAUSED":
|
||||
self.pomodoro["state"] = "ON"
|
||||
self.time = datetime.datetime.now()
|
||||
|
||||
def timer_reset(self, widget):
|
||||
if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "PAUSED":
|
||||
self.pomodoro = {"state": "OFF", "type": ""}
|
||||
self.remaining_time = datetime.timedelta(minutes=self.__work_period)
|
||||
|
||||
def state(self, widget):
|
||||
state = []
|
||||
state.append(self.pomodoro["state"].lower())
|
||||
if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "OFF":
|
||||
state.append(self.pomodoro["type"].lower())
|
||||
|
||||
return state
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
68
bumblebee_status/modules/contrib/prime.py
Normal file
68
bumblebee_status/modules/contrib/prime.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays and changes the current selected prime video card
|
||||
|
||||
Left click will call 'sudo prime-select nvidia'
|
||||
Right click will call 'sudo prime-select nvidia'
|
||||
|
||||
Running these commands without a password requires editing your sudoers file
|
||||
(always use visudo, it's very easy to make a mistake and get locked out of your computer!)
|
||||
|
||||
sudo visudo -f /etc/sudoers.d/prime
|
||||
|
||||
Then put a line like this in there:
|
||||
|
||||
user ALL=(ALL) NOPASSWD: /usr/bin/prime-select
|
||||
|
||||
If you can't figure out the sudoers thing, then don't worry, it's still really useful.
|
||||
|
||||
Parameters:
|
||||
* prime.nvidiastring: String to use when nvidia is selected (defaults to 'intel')
|
||||
* prime.intelstring: String to use when intel is selected (defaults to 'intel')
|
||||
|
||||
Requires the following executable:
|
||||
* prime-select
|
||||
|
||||
contributed by `jeffeb3 <https://github.com/jeffeb3>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.query))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__chooseNvidia)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.__chooseIntel)
|
||||
|
||||
self.nvidiastring = self.parameter("nvidiastring", "nv")
|
||||
self.intelstring = self.parameter("intelstring", "it")
|
||||
|
||||
def __chooseNvidia(self, event):
|
||||
util.cli.execute("sudo prime-select nvidia")
|
||||
|
||||
def __chooseIntel(self, event):
|
||||
util.cli.execute("sudo prime-select intel")
|
||||
|
||||
def query(self, widget):
|
||||
try:
|
||||
res = util.cli.execute("prime-select query")
|
||||
except RuntimeError:
|
||||
return "n/a"
|
||||
|
||||
for line in res.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
if "nvidia" in line:
|
||||
return self.nvidiastring
|
||||
if "intel" in line:
|
||||
return self.intelstring
|
||||
return "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
108
bumblebee_status/modules/contrib/progress.py
Normal file
108
bumblebee_status/modules/contrib/progress.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
"""
|
||||
Show progress for cp, mv, dd, ...
|
||||
|
||||
Parameters:
|
||||
* progress.placeholder: Text to display while no process is running (defaults to 'n/a')
|
||||
* progress.barwidth: Width of the progressbar if it is used (defaults to 8)
|
||||
* progress.format: Format string (defaults to '{bar} {cmd} {arg}')
|
||||
Available values are: {bar} {pid} {cmd} {arg} {percentage} {quantity} {speed} {time}
|
||||
* progress.barfilledchar: Character used to draw the filled part of the bar (defaults to '#'), notice that it can be a string
|
||||
* progress.baremptychar: Character used to draw the empty part of the bar (defaults to '-'), notice that it can be a string
|
||||
|
||||
Requires the following executable:
|
||||
* progress
|
||||
|
||||
contributed by `remi-dupre <https://github.com/remi-dupre>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.get_progress_text))
|
||||
self.__active = False
|
||||
|
||||
def get_progress_text(self, widget):
|
||||
if self.update_progress_info(widget):
|
||||
width = util.format.asint(self.parameter("barwidth", 8))
|
||||
count = round((width * widget.get("per")) / 100)
|
||||
filledchar = self.parameter("barfilledchar", "#")
|
||||
emptychar = self.parameter("baremptychar", "-")
|
||||
|
||||
bar = "[{}{}]".format(filledchar * count, emptychar * (width - count))
|
||||
|
||||
str_format = self.parameter("format", "{bar} {cmd} {arg}")
|
||||
return str_format.format(
|
||||
bar=bar,
|
||||
pid=widget.get("pid"),
|
||||
cmd=widget.get("cmd"),
|
||||
arg=widget.get("arg"),
|
||||
percentage=widget.get("per"),
|
||||
quantity=widget.get("qty"),
|
||||
speed=widget.get("spd"),
|
||||
time=widget.get("tim"),
|
||||
)
|
||||
else:
|
||||
return self.parameter("placeholder", "n/a")
|
||||
|
||||
def update_progress_info(self, widget):
|
||||
"""Update widget's informations about the copy"""
|
||||
if not self.__active:
|
||||
return
|
||||
|
||||
# These regex extracts following groups:
|
||||
# 1. pid
|
||||
# 2. command
|
||||
# 3. arguments
|
||||
# 4. progress (xx.x formated)
|
||||
# 5. quantity (.. unit / .. unit formated)
|
||||
# 6. speed
|
||||
# 7. time remaining
|
||||
extract_nospeed = re.compile(
|
||||
"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\)\n.*"
|
||||
)
|
||||
extract_wtspeed = re.compile(
|
||||
"\[ *(\d*)\] ([a-zA-Z]*) (.*)\n\t(\d*\.*\d*)% \((.*)\) (\d*\.\d .*) remaining (\d*:\d*:\d*)\n.*"
|
||||
)
|
||||
|
||||
try:
|
||||
raw = util.cli.execute("progress -qW 0.1")
|
||||
result = extract_wtspeed.match(raw)
|
||||
|
||||
if not result:
|
||||
# Abord speed measures
|
||||
raw = util.cli.execute("progress -q")
|
||||
result = extract_nospeed.match(raw)
|
||||
|
||||
widget.set("spd", "???.? B/s")
|
||||
widget.set("tim", "??:??:??")
|
||||
else:
|
||||
widget.set("spd", result.group(6))
|
||||
widget.set("tim", result.group(7))
|
||||
|
||||
widget.set("pid", int(result.group(1)))
|
||||
widget.set("cmd", result.group(2))
|
||||
widget.set("arg", result.group(3))
|
||||
widget.set("per", float(result.group(4)))
|
||||
widget.set("qty", result.group(5))
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
self.__active = bool(util.cli.execute("progress -q"))
|
||||
|
||||
def state(self, widget):
|
||||
if self.__active:
|
||||
return "copying"
|
||||
return "pending"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
28
bumblebee_status/modules/contrib/publicip.py
Normal file
28
bumblebee_status/modules/contrib/publicip.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
"""Displays public IP address
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.location
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.public_ip))
|
||||
|
||||
self.__ip = ""
|
||||
|
||||
def public_ip(self, widget):
|
||||
return self.__ip
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__ip = util.location.public_ip()
|
||||
except Exception:
|
||||
self.__ip = "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
61
bumblebee_status/modules/contrib/rotation.py
Normal file
61
bumblebee_status/modules/contrib/rotation.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Shows a widget for each connected screen and allows the user to loop through different orientations.
|
||||
|
||||
Requires the following executable:
|
||||
* xrandr
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
|
||||
possible_orientations = ["normal", "left", "inverted", "right"]
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
def update(self):
|
||||
widgets = self.widgets()
|
||||
for line in util.cli.execute("xrandr -q").split("\n"):
|
||||
if not " connected" in line:
|
||||
continue
|
||||
display = line.split(" ", 2)[0]
|
||||
|
||||
orientation = "normal"
|
||||
for curr_orient in possible_orientations:
|
||||
if (line.split(" ")).count(curr_orient) > 1:
|
||||
orientation = curr_orient
|
||||
break
|
||||
|
||||
widget = self.widget(display)
|
||||
if not widget:
|
||||
widget = self.add_widget(full_text=display, name=display)
|
||||
core.input.register(
|
||||
widget, button=core.input.LEFT_MOUSE, cmd=self.__toggle
|
||||
)
|
||||
widget.set("orientation", orientation)
|
||||
widgets.append(widget)
|
||||
|
||||
def state(self, widget):
|
||||
return widget.get("orientation", "normal")
|
||||
|
||||
def __toggle(self, event):
|
||||
widget = self.widget_by_id(event["instance"])
|
||||
|
||||
# compute new orientation based on current orientation
|
||||
idx = possible_orientations.index(widget.get("orientation"))
|
||||
idx = (idx + 1) % len(possible_orientations)
|
||||
new_orientation = possible_orientations[idx]
|
||||
|
||||
widget.set("orientation", new_orientation)
|
||||
|
||||
util.cli.execute(
|
||||
"xrandr --output {} --rotation {}".format(widget.name, new_orientation)
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
368
bumblebee_status/modules/contrib/rss.py
Normal file
368
bumblebee_status/modules/contrib/rss.py
Normal file
|
@ -0,0 +1,368 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""RSS news ticker
|
||||
|
||||
Fetches rss news items and shows these as a news ticker.
|
||||
Left-clicking will open the full story in a browser.
|
||||
New stories are highlighted.
|
||||
|
||||
Parameters:
|
||||
* rss.feeds : Space-separated list of RSS URLs
|
||||
* rss.length : Maximum length of the module, default is 60
|
||||
|
||||
contributed by `lonesomebyte537 <https://github.com/lonesomebyte537>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import feedparser
|
||||
|
||||
import webbrowser
|
||||
import time
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import json
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class Module(core.module.Module):
|
||||
REFRESH_DELAY = 600
|
||||
SCROLL_SPEED = 3
|
||||
LAYOUT_STYLES_ITEMS = [[1, 1, 1], [3, 3, 2], [2, 3, 3], [3, 2, 3]]
|
||||
HISTORY_FILENAME = ".config/i3/rss.hist"
|
||||
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.ticker_update))
|
||||
|
||||
self._feeds = self.parameter(
|
||||
"feeds", "https://www.espn.com/espn/rss/news"
|
||||
).split(" ")
|
||||
self._feeds_to_update = []
|
||||
self._response = ""
|
||||
|
||||
self._max_title_length = int(self.parameter("length", 60))
|
||||
|
||||
self._items = []
|
||||
self._current_item = None
|
||||
|
||||
self._ticker_offset = 0
|
||||
self._pre_delay = 0
|
||||
self._post_delay = 0
|
||||
|
||||
self._state = []
|
||||
|
||||
self._newspaper_filename = tempfile.mktemp(".html")
|
||||
|
||||
self._last_refresh = 0
|
||||
self._last_update = 0
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self._open)
|
||||
core.input.register(
|
||||
self, button=core.input.RIGHT_MOUSE, cmd=self._create_newspaper
|
||||
)
|
||||
|
||||
self._history = {"ticker": {}, "newspaper": {}}
|
||||
self._load_history()
|
||||
|
||||
def _load_history(self):
|
||||
if os.path.isfile(self.HISTORY_FILENAME):
|
||||
self._history = json.loads(open(self.HISTORY_FILENAME, "r").read())
|
||||
|
||||
def _update_history(self, group):
|
||||
sources = set([i["source"] for i in self._items])
|
||||
self._history[group] = dict(
|
||||
[
|
||||
[s, [i["title"] for i in self._items if i["source"] == s]]
|
||||
for s in sources
|
||||
]
|
||||
)
|
||||
|
||||
def _save_history(self):
|
||||
if not os.path.exists(os.path.dirname(self.HISTORY_FILENAME)):
|
||||
os.makedirs(os.path.dirname(self.HISTORY_FILENAME))
|
||||
open(self.HISTORY_FILENAME, "w").write(json.dumps(self._history))
|
||||
|
||||
def _check_history(self, items, group):
|
||||
for i in items:
|
||||
i["new"] = not (
|
||||
i["source"] in self._history[group]
|
||||
and i["title"] in self._history[group][i["source"]]
|
||||
)
|
||||
|
||||
def _open(self, _):
|
||||
if self._current_item:
|
||||
webbrowser.open(self._current_item["link"])
|
||||
|
||||
def _check_for_image(self, entry):
|
||||
image = next(
|
||||
iter([l["href"] for l in entry["links"] if l["rel"] == "enclosure"]), None
|
||||
)
|
||||
if not image and "media_content" in entry:
|
||||
try:
|
||||
media = sorted(
|
||||
entry["media_content"],
|
||||
key=lambda i: i["height"] if "height" in i else 0,
|
||||
reverse=True,
|
||||
)
|
||||
image = next(
|
||||
iter([i["url"] for i in media if i["medium"] == "image"]), None
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if not image:
|
||||
match = re.search(
|
||||
r"<img[^>]*src\s*=['\']*([^\s^>^'^\']*)['\']*", entry["summary"]
|
||||
)
|
||||
if match:
|
||||
image = match.group(1)
|
||||
return image if image else ""
|
||||
|
||||
def _remove_tags(self, txt):
|
||||
return re.sub("<[^>]*>", "", txt)
|
||||
|
||||
def _create_item(self, entry, url, feed):
|
||||
return {
|
||||
"title": self._remove_tags(entry["title"].replace("\n", " ")),
|
||||
"link": entry["link"],
|
||||
"new": True,
|
||||
"source": url,
|
||||
"summary": self._remove_tags(entry["summary"]),
|
||||
"feed": feed,
|
||||
"image": self._check_for_image(entry),
|
||||
"published": time.mktime(entry.published_parsed)
|
||||
if hasattr(entry, "published_parsed")
|
||||
else 0,
|
||||
}
|
||||
|
||||
def _update_items_from_feed(self, url):
|
||||
parser = feedparser.parse(url)
|
||||
new_items = [
|
||||
self._create_item(entry, url, parser["feed"]["title"])
|
||||
for entry in parser["entries"]
|
||||
]
|
||||
# Check history
|
||||
self._check_history(new_items, "ticker")
|
||||
# Remove the previous items
|
||||
self._items = [i for i in self._items if i["source"] != url]
|
||||
# Add the new items
|
||||
self._items.extend(new_items)
|
||||
# Sort the items on publish date
|
||||
self._items.sort(key=lambda i: i["published"], reverse=True)
|
||||
|
||||
def _check_for_refresh(self):
|
||||
if self._feeds_to_update:
|
||||
# Update one feed at a time to not overload this update cycle
|
||||
url = self._feeds_to_update.pop()
|
||||
self._update_items_from_feed(url)
|
||||
|
||||
if not self._feeds_to_update:
|
||||
self._update_history("ticker")
|
||||
self._save_history()
|
||||
|
||||
if not self._current_item:
|
||||
self._next_item()
|
||||
elif time.time() - self._last_refresh >= self.REFRESH_DELAY:
|
||||
# Populate the list with feeds to update
|
||||
self._feeds_to_update = self._feeds[:]
|
||||
# Update the refresh time
|
||||
self._last_refresh = time.time()
|
||||
|
||||
def _next_item(self):
|
||||
self._ticker_offset = 0
|
||||
self._pre_delay = 2
|
||||
self._post_delay = 4
|
||||
|
||||
if not self._items:
|
||||
return
|
||||
|
||||
# Index of the current element
|
||||
idx = (
|
||||
self._items.index(self._current_item)
|
||||
if self._current_item in self._items
|
||||
else -1
|
||||
)
|
||||
|
||||
# First show new items, else show next
|
||||
new_items = [i for i in self._items if i["new"]]
|
||||
self._current_item = next(
|
||||
iter(new_items), self._items[(idx + 1) % len(self._items)]
|
||||
)
|
||||
|
||||
def _check_scroll_done(self):
|
||||
# Check if the complete title has been shown
|
||||
if self._ticker_offset + self._max_title_length > len(
|
||||
self._current_item["title"]
|
||||
):
|
||||
# Do not immediately show next item after scroll
|
||||
self._post_delay -= 1
|
||||
if self._post_delay == 0:
|
||||
self._current_item["new"] = False
|
||||
# Mark the previous item as 'old'
|
||||
self._next_item()
|
||||
else:
|
||||
# Increase scroll position
|
||||
self._ticker_offset += self.SCROLL_SPEED
|
||||
|
||||
def ticker_update(self, _):
|
||||
# Only update the ticker once a second
|
||||
now = time.time()
|
||||
if now - self._last_update < 1:
|
||||
return self._response
|
||||
|
||||
self._last_update = now
|
||||
|
||||
self._check_for_refresh()
|
||||
|
||||
# If no items were retrieved, return an empty string
|
||||
if not self._current_item:
|
||||
return " " * self._max_title_length
|
||||
|
||||
# Prepare a substring of the item title
|
||||
self._response = self._current_item["title"][
|
||||
self._ticker_offset : self._ticker_offset + self._max_title_length
|
||||
]
|
||||
# Add spaces if too short
|
||||
self._response = self._response.ljust(self._max_title_length)
|
||||
|
||||
# Do not immediately scroll
|
||||
if self._pre_delay > 0:
|
||||
# Change state during pre_delay for new items
|
||||
if self._current_item["new"]:
|
||||
self._state = ["warning"]
|
||||
self._pre_delay -= 1
|
||||
return self._response
|
||||
|
||||
self._state = []
|
||||
self._check_scroll_done()
|
||||
|
||||
return self._response
|
||||
|
||||
def state(self, _):
|
||||
return self._state
|
||||
|
||||
def _create_news_element(self, item, overlay_title):
|
||||
try:
|
||||
timestr = (
|
||||
"" if item["published"] == 0 else str(time.ctime(item["published"]))
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.error(str(exc))
|
||||
raise e
|
||||
element = "<div class='item' onclick=window.open('" + item["link"] + "')>"
|
||||
element += "<div class='titlecontainer'>"
|
||||
element += (
|
||||
" <img "
|
||||
+ ("" if item["image"] else "class='noimg' ")
|
||||
+ "src='"
|
||||
+ item["image"]
|
||||
+ "'>"
|
||||
)
|
||||
element += (
|
||||
" <div class='title"
|
||||
+ (" overlay" if overlay_title else "")
|
||||
+ "'>"
|
||||
+ ("<span class='star'>★</span>" if item["new"] else "")
|
||||
+ item["title"]
|
||||
+ "</div>"
|
||||
)
|
||||
element += "</div>"
|
||||
element += "<div class='summary'>" + item["summary"] + "</div>"
|
||||
element += (
|
||||
"<div class='info'><span class='author'>"
|
||||
+ item["feed"]
|
||||
+ "</span><span class='published'>"
|
||||
+ timestr
|
||||
+ "</span></div>"
|
||||
)
|
||||
element += "</div>"
|
||||
return element
|
||||
|
||||
def _create_news_section(self, newspaper_items):
|
||||
style = random.randint(0, 3)
|
||||
section = "<table><tr class='style" + str(style) + "'>"
|
||||
for i in range(0, 3):
|
||||
section += "<td><div class='itemcontainer'>"
|
||||
for _ in range(0, self.LAYOUT_STYLES_ITEMS[style][i]):
|
||||
if newspaper_items:
|
||||
section += self._create_news_element(
|
||||
newspaper_items[0], self.LAYOUT_STYLES_ITEMS[style][i] != 3
|
||||
)
|
||||
del newspaper_items[0]
|
||||
section += "</div></td>"
|
||||
section += "</tr></table>"
|
||||
return section
|
||||
|
||||
def _create_newspaper(self, _):
|
||||
content = ""
|
||||
newspaper_items = self._items[:]
|
||||
self._check_history(newspaper_items, "newspaper")
|
||||
|
||||
# Make sure new items are always listed first, independent of publish date
|
||||
newspaper_items.sort(
|
||||
key=lambda i: i["published"] + (10000000 if i["new"] else 0), reverse=True
|
||||
)
|
||||
|
||||
while newspaper_items:
|
||||
content += self._create_news_section(newspaper_items)
|
||||
open(self._newspaper_filename, "w").write(
|
||||
HTML_TEMPLATE.replace("[[CONTENT]]", content)
|
||||
)
|
||||
webbrowser.open("file://" + self._newspaper_filename)
|
||||
self._update_history("newspaper")
|
||||
self._save_history()
|
||||
|
||||
|
||||
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
var images = document.getElementsByTagName('img');
|
||||
// Remove very small images
|
||||
for(var i = 0; i < images.length; i++) {
|
||||
if (images[i].naturalWidth<50 || images[i].naturalHeight<50) {
|
||||
images[i].src = ''
|
||||
images[i].className+=' noimg'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<style>
|
||||
body {background: #eee; font-family: Helvetica neue;}
|
||||
td {background: #fff; height: 100%;}
|
||||
tr.style0 td {width: 33%;}
|
||||
tr.style1 td {width: 20%;}
|
||||
tr.style1 td:last-child {width: 60%;}
|
||||
tr.style2 td {width: 20%;}
|
||||
tr.style2 td:first-child {width: 60%;}
|
||||
tr.style3 td {width: 20%;}
|
||||
tr.style3 td:nth-child(2) {width: 60%;}
|
||||
img {width: 100%; display: block; }
|
||||
img.noimg {min-height:250px; background: #1299c8;}
|
||||
#content {width: 1500px; margin: auto; background: #eee; padding: 1px;}
|
||||
#newspapertitle {text-align: center; font-size: 60px; font-family: Arial Black; background: #1299c8; font-style: Italic; padding: 10px; color: #fff; }
|
||||
.star {color: #ffa515; font-size: 24px;}
|
||||
.section {display: flex;}
|
||||
.column {display: flex;}
|
||||
.itemcontainer {width: 100%; height: 100%; position: relative; display: inline-table;}
|
||||
.item {cursor: pointer; }
|
||||
.titlecontainer {position: relative;}
|
||||
.title.overlay {font-family: Arial; position: absolute; bottom: 10px; color: #fff; font-weight: bold; text-align: right; max-width: 75%; right: 10px; font-size: 23px; text-shadow: 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000, -1px 0 0 #000;}
|
||||
.title:not(.overlay) {font-weight: bold; padding: 0px 10px;}
|
||||
.summary {color: #444; padding: 10px 10px 0px 10px; font-family: Times new roman; font-size: 18px; flex: 1;max-height: 105px; overflow: hidden;}
|
||||
.info {color: #aaa; font-family: arial; font-size: 13px; padding: 10px;}
|
||||
.published {float: right;}
|
||||
</style>
|
||||
<body>
|
||||
<div id='content'>
|
||||
<div id='newspapertitle'>Bumblebee Daily</div>
|
||||
[[CONTENT]]
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
148
bumblebee_status/modules/contrib/sensors.py
Normal file
148
bumblebee_status/modules/contrib/sensors.py
Normal file
|
@ -0,0 +1,148 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays sensor temperature
|
||||
|
||||
Parameters:
|
||||
* sensors.path: path to temperature file (default /sys/class/thermal/thermal_zone0/temp).
|
||||
* sensors.json: if set to 'true', interpret sensors.path as JSON 'path' in the output
|
||||
of 'sensors -j' (i.e. <key1>/<key2>/.../<value>), for example, path could
|
||||
be: 'coretemp-isa-00000/Core 0/temp1_input' (defaults to 'false')
|
||||
* sensors.match: (fallback) Line to match against output of 'sensors -u' (default: temp1_input)
|
||||
* sensors.match_pattern: (fallback) Line to match against before temperature is read (no default)
|
||||
* sensors.match_number: (fallback) which of the matches you want (default -1: last match).
|
||||
* sensors.show_freq: whether to show CPU frequency. (default: true)
|
||||
|
||||
|
||||
contributed by `mijoharas <https://github.com/mijoharas>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.temperature))
|
||||
|
||||
self._temperature = "unknown"
|
||||
self._mhz = "n/a"
|
||||
self._match_number = int(self.parameter("match_number", "-1"))
|
||||
self._match_pattern = self.parameter("match_pattern", None)
|
||||
self._pattern = re.compile(
|
||||
r"^\s*{}:\s*([\d.]+)$".format(self.parameter("match", "temp1_input")),
|
||||
re.MULTILINE,
|
||||
)
|
||||
self._json = util.format.asbool(self.parameter("json", False))
|
||||
self._freq = util.format.asbool(self.parameter("show_freq", True))
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd="xsensors")
|
||||
self.determine_method()
|
||||
|
||||
def determine_method(self):
|
||||
if self.parameter("path") != None and self._json == False:
|
||||
self.use_sensors = False # use thermal zone
|
||||
else:
|
||||
# try to use output of sensors -u
|
||||
try:
|
||||
output = util.cli.execute("sensors -u")
|
||||
self.use_sensors = True
|
||||
log.debug("Sensors command available")
|
||||
except FileNotFoundError as e:
|
||||
log.info(
|
||||
"Sensors command not available, using /sys/class/thermal/thermal_zone*/"
|
||||
)
|
||||
self.use_sensors = False
|
||||
|
||||
def _get_temp_from_sensors(self):
|
||||
if self._json == True:
|
||||
try:
|
||||
output = json.loads(util.cli.execute("sensors -j"))
|
||||
for key in self.parameter("path").split("/"):
|
||||
output = output[key]
|
||||
return int(float(output))
|
||||
except Exception as e:
|
||||
logging.error("unable to read sensors: {}".format(str(e)))
|
||||
return "unknown"
|
||||
else:
|
||||
output = util.cli.execute("sensors -u")
|
||||
if self._match_pattern:
|
||||
temp_pattern = self.parameter("match", "temp1_input")
|
||||
match = re.search(
|
||||
r"{}.+{}:\s*([\d.]+)$".format(self._match_pattern, temp_pattern),
|
||||
output.replace("\n", ""),
|
||||
)
|
||||
if match:
|
||||
return int(float(match.group(1)))
|
||||
else:
|
||||
return "unknown"
|
||||
match = self._pattern.findall(output)
|
||||
if match:
|
||||
return int(float(match[self._match_number]))
|
||||
return "unknown"
|
||||
|
||||
def get_temp(self):
|
||||
if self.use_sensors:
|
||||
temperature = self._get_temp_from_sensors()
|
||||
log.debug("Retrieve temperature from sensors -u")
|
||||
else:
|
||||
try:
|
||||
temperature = open(
|
||||
self.parameter("path", "/sys/class/thermal/thermal_zone0/temp")
|
||||
).read()[:2]
|
||||
log.debug("retrieved temperature from /sys/class/")
|
||||
# TODO: Iterate through all thermal zones to determine the correct one and use its value
|
||||
# https://unix.stackexchange.com/questions/304845/discrepancy-between-number-of-cores-and-thermal-zones-in-sys-class-thermal
|
||||
|
||||
except IOError:
|
||||
temperature = "unknown"
|
||||
log.info("Can not determine temperature, please install lm-sensors")
|
||||
|
||||
return temperature
|
||||
|
||||
def get_mhz(self):
|
||||
mhz = None
|
||||
try:
|
||||
output = open(
|
||||
"/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"
|
||||
).read()
|
||||
mhz = int(float(output) / 1000.0)
|
||||
except:
|
||||
output = open("/proc/cpuinfo").read()
|
||||
m = re.search(r"cpu MHz\s+:\s+(\d+)", output)
|
||||
if m:
|
||||
mhz = int(m.group(1))
|
||||
else:
|
||||
m = re.search(r"BogoMIPS\s+:\s+(\d+)", output)
|
||||
if m:
|
||||
return "{} BogoMIPS".format(int(m.group(1)))
|
||||
if not mhz:
|
||||
return "n/a"
|
||||
|
||||
if mhz < 1000:
|
||||
return "{} MHz".format(mhz)
|
||||
else:
|
||||
return "{:0.01f} GHz".format(float(mhz) / 1000.0)
|
||||
|
||||
def temperature(self, _):
|
||||
if self._freq:
|
||||
return "{}°c @ {}".format(self._temperature, self._mhz)
|
||||
else:
|
||||
return "{}°c".format(self._temperature)
|
||||
|
||||
def update(self):
|
||||
self._temperature = self.get_temp()
|
||||
if self._freq:
|
||||
self._mhz = self.get_mhz()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
84
bumblebee_status/modules/contrib/shell.py
Normal file
84
bumblebee_status/modules/contrib/shell.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# pylint: disable=C0111,R0903,W1401
|
||||
|
||||
""" Execute command in shell and print result
|
||||
|
||||
Few command examples:
|
||||
'ping -c 1 1.1.1.1 | grep -Po '(?<=time=)\d+(\.\d+)? ms''
|
||||
'echo 'BTC=$(curl -s rate.sx/1BTC | grep -Po \'^\d+\')USD''
|
||||
'curl -s https://wttr.in/London?format=%l+%t+%h+%w'
|
||||
'pip3 freeze | wc -l'
|
||||
'any_custom_script.sh | grep arguments'
|
||||
|
||||
Parameters:
|
||||
* shell.command: Command to execute
|
||||
Use single parentheses if evaluating anything inside (sh-style)
|
||||
For example shell.command='echo $(date +'%H:%M:%S')'
|
||||
But NOT shell.command='echo $(date +'%H:%M:%S')'
|
||||
Second one will be evaluated only once at startup
|
||||
* shell.interval: Update interval in seconds
|
||||
(defaults to 1s == every bumblebee-status update)
|
||||
* shell.async: Run update in async mode. Won't run next thread if
|
||||
previous one didn't finished yet. Useful for long
|
||||
running scripts to avoid bumblebee-status freezes
|
||||
(defaults to False)
|
||||
|
||||
contributed by `rrhuffy <https://github.com/rrhuffy>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import util.format
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.get_output))
|
||||
|
||||
self.__command = self.parameter("command", 'echo "no command configured"')
|
||||
self.__async = util.format.asbool(self.parameter("async"))
|
||||
|
||||
if self.__async:
|
||||
self.__output = "please wait..."
|
||||
self.__current_thread = threading.Thread()
|
||||
|
||||
# LMB and RMB will update output regardless of timer
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.update)
|
||||
core.input.register(self, button=core.input.RIGHT_MOUSE, cmd=self.update)
|
||||
|
||||
def set_output(self, value):
|
||||
self.__output = value
|
||||
|
||||
def get_output(self, _):
|
||||
return self.__output
|
||||
|
||||
def update(self):
|
||||
# if requested then run not async version and just execute command in this thread
|
||||
if not self.__async:
|
||||
self.__output = util.cli.execute(self.__command, ignore_errors=True).strip()
|
||||
return
|
||||
|
||||
# if previous thread didn't end yet then don't do anything
|
||||
if self.__current_thread.is_alive():
|
||||
return
|
||||
|
||||
# spawn new thread to execute command and pass callback method to get output from it
|
||||
self.__current_thread = threading.Thread(
|
||||
target=lambda obj, cmd: obj.set_output(
|
||||
util.cli.execute(cmd, ignore_errors=True)
|
||||
),
|
||||
args=(self, self.__command),
|
||||
)
|
||||
self.__current_thread.start()
|
||||
|
||||
def state(self, _):
|
||||
if self.__output == "no command configured":
|
||||
return "warning"
|
||||
|
||||
|
||||
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
|
69
bumblebee_status/modules/contrib/shortcut.py
Normal file
69
bumblebee_status/modules/contrib/shortcut.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
# pylint: disable=C0112,R0903
|
||||
|
||||
"""Shows a widget per user-defined shortcut and allows to define the behaviour
|
||||
when clicking on it.
|
||||
|
||||
For more than one shortcut, the commands and labels are strings separated by
|
||||
a demiliter (; semicolon by default).
|
||||
|
||||
For example in order to create two shortcuts labeled A and B with commands
|
||||
cmdA and cmdB you could do:
|
||||
|
||||
./bumblebee-status -m shortcut -p shortcut.cmd='ls;ps' shortcut.label='A;B'
|
||||
|
||||
Parameters:
|
||||
* shortcut.cmds : List of commands to execute
|
||||
* shortcut.labels: List of widgets' labels (text)
|
||||
* shortcut.delim : Commands and labels delimiter (; semicolon by default)
|
||||
|
||||
|
||||
contributed by `cacyss0807 <https://github.com/cacyss0807>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
LINK = "https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
|
||||
LABEL = "Click me"
|
||||
|
||||
import core.module
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.__labels = self.parameter("labels", "{}".format(LABEL))
|
||||
self.__cmds = self.parameter("cmds", "firefox {}".format(LINK))
|
||||
self.__delim = self.parameter("delim", ";")
|
||||
|
||||
self.update_widgets()
|
||||
|
||||
def update_widgets(self):
|
||||
""" Creates a set of widget per user define shortcut."""
|
||||
|
||||
cmds = self.__cmds.split(self.__delim)
|
||||
labels = self.__labels.split(self.__delim)
|
||||
|
||||
# to be on the safe side create as many widgets as there are data (cmds or labels)
|
||||
num_shortcuts = min(len(cmds), len(labels))
|
||||
|
||||
# report possible problem as a warning
|
||||
if len(cmds) is not len(labels):
|
||||
logging.warning(
|
||||
"shortcut: the number of commands does not match "
|
||||
"the number of provided labels."
|
||||
)
|
||||
logging.warning("cmds : %s, labels : %s", cmds, labels)
|
||||
|
||||
for idx in range(0, num_shortcuts):
|
||||
cmd = cmds[idx]
|
||||
label = labels[idx]
|
||||
|
||||
widget = self.add_widget(full_text=label)
|
||||
core.input.register(widget, button=core.input.LEFT_MOUSE, cmd=cmd)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
127
bumblebee_status/modules/contrib/smartstatus.py
Normal file
127
bumblebee_status/modules/contrib/smartstatus.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
|
||||
# smart function inspired by py-SMART https://github.com/freenas/py-SMART
|
||||
# under Copyright (C) 2015 Marc Herndon and GPL2
|
||||
|
||||
"""Displays HDD smart status of different drives or all drives
|
||||
|
||||
Parameters:
|
||||
* smartstatus.display: how to display (defaults to 'combined', other choices: 'seperate' or 'singles')
|
||||
* smartstatus.drives: in the case of singles which drives to display, separated comma list value, multiple accepted (defaults to 'sda', example:'sda,sdc')
|
||||
* smartstatus.show_names: boolean in the form of "True" or "False" to show the name of the drives in the form of sda, sbd, combined or none at all.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import shutil
|
||||
|
||||
import core.module
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self.devices = self.list_devices()
|
||||
self.display = self.parameter("display", "combined")
|
||||
self.drives = self.parameter("drives", "sda")
|
||||
self.show_names = util.format.asbool(self.parameter("show_names", True))
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
if self.display == "combined":
|
||||
widget = self.add_widget()
|
||||
widget.set("device", "combined")
|
||||
widget.set("assessment", self.combined())
|
||||
self.output(widget)
|
||||
else:
|
||||
for device in self.devices:
|
||||
if self.display == "singles" and device not in self.drives:
|
||||
continue
|
||||
widget = self.add_widget()
|
||||
widget.set("device", device)
|
||||
widget.set("assessment", self.smart(device))
|
||||
self.output(widget)
|
||||
|
||||
def update(self):
|
||||
for widget in self.widgets():
|
||||
device = widget.get("device")
|
||||
if device == "combined":
|
||||
widget.set("assessment", self.combined())
|
||||
self.output(widget)
|
||||
else:
|
||||
widget.set("assessment", self.smart(device))
|
||||
self.output(widget)
|
||||
|
||||
def output(self, widget):
|
||||
device = widget.get("device")
|
||||
assessment = widget.get("assessment")
|
||||
if self.show_names:
|
||||
widget.full_text("{}: {}".format(device, assessment))
|
||||
else:
|
||||
widget.full_text("{}".format(assessment))
|
||||
|
||||
def state(self, widget):
|
||||
states = []
|
||||
assessment = widget.get("assessment")
|
||||
if assessment == "Pre-fail":
|
||||
states.append("warning")
|
||||
if assessment == "Fail":
|
||||
states.append("critical")
|
||||
return states
|
||||
|
||||
def combined(self):
|
||||
for device in self.devices:
|
||||
result = self.smart(device)
|
||||
if result == "Fail":
|
||||
return "Fail"
|
||||
if result == "Pre-fail":
|
||||
return "Pre-fail"
|
||||
return "OK"
|
||||
|
||||
def list_devices(self):
|
||||
for (root, folders, files) in os.walk("/dev"):
|
||||
if root == "/dev":
|
||||
devices = {
|
||||
"".join(filter(lambda i: i.isdigit() == False, file))
|
||||
for file in files
|
||||
if "sd" in file
|
||||
}
|
||||
nvme = {
|
||||
file for file in files if ("nvme0n" in file and "p" not in file)
|
||||
}
|
||||
devices.update(nvme)
|
||||
return devices
|
||||
|
||||
def smart(self, disk_name):
|
||||
smartctl = shutil.which("smartctl")
|
||||
assessment = None
|
||||
|
||||
output = util.cli.execute(
|
||||
"sudo {} --health {}".format(smartctl, os.path.join("/dev/", disk_name))
|
||||
)
|
||||
output = output.split("\n")
|
||||
line = output[4]
|
||||
if "SMART" in line:
|
||||
if any([i in line for i in ["PASSED", "OK"]]):
|
||||
assessment = "OK"
|
||||
else:
|
||||
assessment = "Fail"
|
||||
|
||||
if assessment == "OK":
|
||||
output = util.cli.execute(
|
||||
"sudo {} -A {}".format(smartctl, os.path.join("/dev/", disk_name))
|
||||
)
|
||||
output = output.split("\n")
|
||||
for line in output:
|
||||
if "Pre-fail" in line:
|
||||
assessment = "Pre-fail"
|
||||
return assessment
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
149
bumblebee_status/modules/contrib/spaceapi.py
Normal file
149
bumblebee_status/modules/contrib/spaceapi.py
Normal file
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the state of a Space API endpoint
|
||||
Space API is an API for hackspaces based on JSON. See spaceapi.io for
|
||||
an example.
|
||||
|
||||
Requires the following libraries:
|
||||
* requests
|
||||
* regex
|
||||
|
||||
Parameters:
|
||||
* spaceapi.url: String representation of the api endpoint
|
||||
* spaceapi.format: Format string for the output
|
||||
|
||||
Format Strings:
|
||||
* Format strings are indicated by double %%
|
||||
* They represent a leaf in the JSON tree, layers seperated by '.'
|
||||
* Boolean values can be overwritten by appending '%true%false'
|
||||
in the format string
|
||||
* Example: to reference 'open' in '{'state':{'open': true}}'
|
||||
you would write '%%state.open%%', if you also want
|
||||
to say 'Open/Closed' depending on the boolean you
|
||||
would write '%%state.open%Open%Closed%%'
|
||||
|
||||
contributed by `rad4day <https://github.com/rad4day>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import requests
|
||||
import threading
|
||||
import re
|
||||
import json
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
|
||||
def formatStringBuilder(s, json):
|
||||
"""
|
||||
Parses Format Strings
|
||||
Parameter:
|
||||
s -> format string
|
||||
json -> the spaceapi response object
|
||||
"""
|
||||
identifiers = re.findall("%%.*?%%", s)
|
||||
for i in identifiers:
|
||||
ic = i[2:-2] # Discard %%
|
||||
j = ic.split("%")
|
||||
|
||||
# Only neither of, or both true AND false may be overwritten
|
||||
if len(j) != 3 and len(j) != 1:
|
||||
return "INVALID FORMAT STRING"
|
||||
|
||||
if len(j) == 1: # no overwrite
|
||||
s = s.replace(i, json[j[0]])
|
||||
elif json[j[0]]: # overwrite for True
|
||||
s = s.replace(i, j[1])
|
||||
else: # overwrite for False
|
||||
s = s.replace(i, j[2])
|
||||
return s
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=15)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.getState))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.__forceReload)
|
||||
|
||||
self.__data = {}
|
||||
self.__error = None
|
||||
self.__thread = None
|
||||
|
||||
# The URL representing the api endpoint
|
||||
self.__url = self.parameter("url", default="http://club.entropia.de/spaceapi")
|
||||
self._format = self.parameter(
|
||||
"format", default=" %%space%%: %%state.open%Open%Closed%%"
|
||||
)
|
||||
|
||||
def state(self, widget):
|
||||
try:
|
||||
if self.__error is not None:
|
||||
return ["critical"]
|
||||
elif self.__data["state.open"]:
|
||||
return ["warning"]
|
||||
else:
|
||||
return []
|
||||
except KeyError:
|
||||
return ["critical"]
|
||||
|
||||
def update(self):
|
||||
if not self.__thread or self.__thread.is_alive() == False:
|
||||
self.__thread = threading.Thread(target=self.get_api_async, args=())
|
||||
self.__thread.start()
|
||||
|
||||
def getState(self, widget):
|
||||
text = self._format
|
||||
if self.__error is not None:
|
||||
text = self.__error
|
||||
else:
|
||||
try:
|
||||
text = formatStringBuilder(self._format, self.__data)
|
||||
except KeyError:
|
||||
text = "KeyError"
|
||||
return text
|
||||
|
||||
def get_api_async(self):
|
||||
try:
|
||||
with requests.get(self.__url, timeout=10) as request:
|
||||
# Can't implement error handling for python2.7 if I use
|
||||
# request.json() as it uses simplejson in newer versions
|
||||
self.__data = self.__flatten(json.loads(request.text))
|
||||
self.__error = None
|
||||
except requests.exceptions.Timeout:
|
||||
self.__error = "Timeout"
|
||||
except requests.exceptions.HTTPError:
|
||||
self.__error = "HTTP Error"
|
||||
except ValueError:
|
||||
self.__error = "Not a JSON response"
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
|
||||
# left_mouse_button handler
|
||||
def __forceReload(self, event):
|
||||
if self.__thread:
|
||||
self.__thread.raise_exception()
|
||||
self.__error = "RELOADING"
|
||||
core.event.trigger("update", [self.id], redraw_only=True)
|
||||
|
||||
# Flattens the JSON structure recursively, e.g. ['space']['open']
|
||||
# becomes ['space.open']
|
||||
def __flatten(self, json):
|
||||
out = {}
|
||||
for key in json:
|
||||
value = json[key]
|
||||
if type(value) is dict:
|
||||
flattened_key = self.__flatten(value)
|
||||
for fk in flattened_key:
|
||||
out[key + "." + fk] = flattened_key[fk]
|
||||
else:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
# Author: Tobias Manske <tobias@chaoswg.xyz>
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
95
bumblebee_status/modules/contrib/spotify.py
Normal file
95
bumblebee_status/modules/contrib/spotify.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current song being played
|
||||
|
||||
Requires the following library:
|
||||
* python-dbus
|
||||
|
||||
Parameters:
|
||||
* spotify.format: Format string (defaults to '{artist} - {title}')
|
||||
Available values are: {album}, {title}, {artist}, {trackNumber}, {playbackStatus}
|
||||
* spotify.previous: Change binding for previous song (default is left click)
|
||||
* spotify.next: Change binding for next song (default is right click)
|
||||
* spotify.pause: Change binding for toggling pause (default is middle click)
|
||||
|
||||
Available options for spotify.previous, spotify.next and spotify.pause are:
|
||||
LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN
|
||||
|
||||
|
||||
contributed by `yvesh <https://github.com/yvesh>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import sys
|
||||
import dbus
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.spotify))
|
||||
|
||||
buttons = {
|
||||
"LEFT_CLICK": core.input.LEFT_MOUSE,
|
||||
"RIGHT_CLICK": core.input.RIGHT_MOUSE,
|
||||
"MIDDLE_CLICK": core.input.MIDDLE_MOUSE,
|
||||
"SCROLL_UP": core.input.WHEEL_UP,
|
||||
"SCROLL_DOWN": core.input.WHEEL_DOWN,
|
||||
}
|
||||
|
||||
self.__song = ""
|
||||
self.__format = self.parameter("format", "{artist} - {title}")
|
||||
prev_button = self.parameter("previous", "LEFT_CLICK")
|
||||
next_button = self.parameter("next", "RIGHT_CLICK")
|
||||
pause_button = self.parameter("pause", "MIDDLE_CLICK")
|
||||
|
||||
cmd = "dbus-send --session --type=method_call --dest=org.mpris.MediaPlayer2.spotify \
|
||||
/org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player."
|
||||
core.input.register(self, button=buttons[prev_button], cmd=cmd + "Previous")
|
||||
core.input.register(self, button=buttons[next_button], cmd=cmd + "Next")
|
||||
core.input.register(self, button=buttons[pause_button], cmd=cmd + "PlayPause")
|
||||
|
||||
@core.decorators.scrollable
|
||||
def spotify(self, widget):
|
||||
return self.string_song
|
||||
|
||||
def hidden(self):
|
||||
return self.string_song == ""
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
bus = dbus.SessionBus()
|
||||
spotify = bus.get_object(
|
||||
"org.mpris.MediaPlayer2.spotify", "/org/mpris/MediaPlayer2"
|
||||
)
|
||||
spotify_iface = dbus.Interface(spotify, "org.freedesktop.DBus.Properties")
|
||||
props = spotify_iface.Get("org.mpris.MediaPlayer2.Player", "Metadata")
|
||||
playback_status = str(
|
||||
spotify_iface.Get("org.mpris.MediaPlayer2.Player", "PlaybackStatus")
|
||||
)
|
||||
self.__song = self.__format.format(
|
||||
album=str(props.get("xesam:album")),
|
||||
title=str(props.get("xesam:title")),
|
||||
artist=",".join(props.get("xesam:artist")),
|
||||
trackNumber=str(props.get("xesam:trackNumber")),
|
||||
playbackStatus="\u25B6"
|
||||
if playback_status == "Playing"
|
||||
else "\u258D\u258D"
|
||||
if playback_status == "Paused"
|
||||
else "",
|
||||
)
|
||||
|
||||
except Exception:
|
||||
self.__song = ""
|
||||
|
||||
@property
|
||||
def string_song(self):
|
||||
if sys.version_info.major < 3:
|
||||
return unicode(self.__song)
|
||||
return str(self.__song)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
68
bumblebee_status/modules/contrib/stock.py
Normal file
68
bumblebee_status/modules/contrib/stock.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Display a stock quote from worldtradingdata.com
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
|
||||
Parameters:
|
||||
* stock.symbols : Comma-separated list of symbols to fetch
|
||||
* stock.change : Should we fetch change in stock value (defaults to True)
|
||||
|
||||
|
||||
contributed by `msoulier <https://github.com/msoulier>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
import logging
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(hours=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.value))
|
||||
|
||||
self.__symbols = self.parameter("symbols", "")
|
||||
self.__change = util.format.asbool(self.parameter("change", True))
|
||||
self.__value = None
|
||||
|
||||
def value(self, widget):
|
||||
results = []
|
||||
if not self.__value:
|
||||
return "n/a"
|
||||
data = json.loads(self.__value)
|
||||
|
||||
for symbol in data["quoteResponse"]["result"]:
|
||||
valkey = "regularMarketChange" if self.__change else "regularMarketPrice"
|
||||
sym = symbol.get("symbol", "n/a")
|
||||
currency = symbol.get("currency", "USD")
|
||||
val = "n/a" if not valkey in symbol else "{:.2f}".format(symbol[valkey])
|
||||
results.append("{} {} {}".format(sym, val, currency))
|
||||
return " ".join(results)
|
||||
|
||||
def fetch(self):
|
||||
if self.__symbols:
|
||||
url = "https://query1.finance.yahoo.com/v7/finance/quote?symbols="
|
||||
url += (
|
||||
self.__symbols
|
||||
+ "&fields=regularMarketPrice,currency,regularMarketChange"
|
||||
)
|
||||
return urllib.request.urlopen(url).read().strip()
|
||||
else:
|
||||
logging.error("unable to retrieve stock exchange rate")
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
self.__value = self.fetch()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
99
bumblebee_status/modules/contrib/sun.py
Normal file
99
bumblebee_status/modules/contrib/sun.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays sunrise and sunset times
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
* suntime
|
||||
|
||||
Parameters:
|
||||
* cpu.lat : Latitude of your location
|
||||
* cpu.lon : Longitude of your location
|
||||
|
||||
(if none of those are set, location is determined automatically via location APIs)
|
||||
|
||||
contributed by `lonesomebyte537 <https://github.com/lonesomebyte537>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from suntime import Sun, SunTimeException
|
||||
import requests
|
||||
from dateutil.tz import tzlocal
|
||||
|
||||
import datetime
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
import util.location
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(hours=1)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.suntimes))
|
||||
|
||||
lat = self.parameter("lat", None)
|
||||
lon = self.parameter("lon", None)
|
||||
self.__sun = None
|
||||
|
||||
if not lat or not lon:
|
||||
lat, lon = util.location.coordinates()
|
||||
if lat and lon:
|
||||
self.__sun = Sun(float(lat), float(lon))
|
||||
|
||||
def suntimes(self, _):
|
||||
if self.__sunset and self.__sunrise:
|
||||
if self.__isup:
|
||||
return "\u21A7{} \u21A5{}".format(
|
||||
self.__sunset.strftime("%H:%M"), self.__sunrise.strftime("%H:%M")
|
||||
)
|
||||
return "\u21A5{} \u21A7{}".format(
|
||||
self.__sunrise.strftime("%H:%M"), self.__sunset.strftime("%H:%M")
|
||||
)
|
||||
return "n/a"
|
||||
|
||||
def __calculate_times(self):
|
||||
self.__isup = False
|
||||
|
||||
order_matters = True
|
||||
|
||||
try:
|
||||
self.__sunrise = self.__sun.get_local_sunrise_time()
|
||||
except SunTimeException:
|
||||
self.__sunrise = "no sunrise"
|
||||
order_matters = False
|
||||
|
||||
try:
|
||||
self.__sunset = self.__sun.get_local_sunset_time()
|
||||
except SunTimeException:
|
||||
self.__sunset = "no sunset"
|
||||
order_matters = False
|
||||
|
||||
if not order_matters:
|
||||
return
|
||||
|
||||
now = datetime.datetime.now(tz=tzlocal())
|
||||
if now > self.__sunset:
|
||||
tomorrow = (now + datetime.timedelta(days=1)).date()
|
||||
try:
|
||||
self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow)
|
||||
self.__sunset = self.__sun.get_local_sunset_time(tomorrow)
|
||||
except SunTimeException:
|
||||
self.__sunrise = "no sunrise"
|
||||
self.__sunset = "no sunset"
|
||||
|
||||
elif now > self.__sunrise:
|
||||
tomorrow = (now + datetime.timedelta(days=1)).date()
|
||||
try:
|
||||
self.__sunrise = self.__sun.get_local_sunrise_time(tomorrow)
|
||||
except SunTimeException:
|
||||
self.__sunrise = "no sunrise"
|
||||
return
|
||||
self.__isup = True
|
||||
|
||||
def update(self):
|
||||
self.__calculate_times()
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
116
bumblebee_status/modules/contrib/system.py
Normal file
116
bumblebee_status/modules/contrib/system.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
""" system module
|
||||
|
||||
adds the possibility to
|
||||
* shutdown
|
||||
* reboot
|
||||
|
||||
the system.
|
||||
|
||||
Per default a confirmation dialog is shown before the actual action is performed.
|
||||
|
||||
Parameters:
|
||||
* system.confirm: show confirmation dialog before performing any action (default: true)
|
||||
* system.reboot: specify a reboot command (defaults to 'reboot')
|
||||
* system.shutdown: specify a shutdown command (defaults to 'shutdown -h now')
|
||||
* system.logout: specify a logout command (defaults to 'i3exit logout')
|
||||
* system.switch_user: specify a command for switching the user (defaults to 'i3exit switch_user')
|
||||
* system.lock: specify a command for locking the screen (defaults to 'i3exit lock')
|
||||
* system.suspend: specify a command for suspending (defaults to 'i3exit suspend')
|
||||
* system.hibernate: specify a command for hibernating (defaults to 'i3exit hibernate')
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox as tkmessagebox
|
||||
except ImportError:
|
||||
logging.warning("failed to import tkinter - bumblebee popups won't work!")
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
import util.popup
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.text))
|
||||
|
||||
self.__confirm = util.format.asbool(self.parameter("confirm", True))
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
|
||||
|
||||
def text(self, widget):
|
||||
return ""
|
||||
|
||||
def __on_command(self, header, text, command):
|
||||
do_it = True
|
||||
if self.__confirm:
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.focus_set()
|
||||
|
||||
do_it = tkmessagebox.askyesno(header, text)
|
||||
root.destroy()
|
||||
|
||||
if do_it:
|
||||
util.cli.execute(command)
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu()
|
||||
reboot_cmd = self.parameter("reboot", "reboot")
|
||||
shutdown_cmd = self.parameter("shutdown", "shutdown -h now")
|
||||
logout_cmd = self.parameter("logout", "i3exit logout")
|
||||
switch_user_cmd = self.parameter("switch_user", "i3exit switch_user")
|
||||
lock_cmd = self.parameter("lock", "i3exit lock")
|
||||
suspend_cmd = self.parameter("suspend", "i3exit suspend")
|
||||
hibernate_cmd = self.parameter("hibernate", "i3exit hibernate")
|
||||
|
||||
menu.add_menuitem(
|
||||
"shutdown",
|
||||
callback=functools.partial(
|
||||
self.__on_command, "Shutdown", "Shutdown?", shutdown_cmd
|
||||
),
|
||||
)
|
||||
menu.add_menuitem(
|
||||
"reboot",
|
||||
callback=functools.partial(
|
||||
self.__on_command, "Reboot", "Reboot?", reboot_cmd
|
||||
),
|
||||
)
|
||||
menu.add_menuitem(
|
||||
"log out",
|
||||
callback=functools.partial(
|
||||
self.__on_command, "Log out", "Log out?", "i3exit logout"
|
||||
),
|
||||
)
|
||||
# don't ask for these
|
||||
menu.add_menuitem(
|
||||
"switch user", callback=functools.partial(util.cli.execute, switch_user_cmd)
|
||||
)
|
||||
menu.add_menuitem(
|
||||
"lock", callback=functools.partial(util.cli.execute, lock_cmd)
|
||||
)
|
||||
menu.add_menuitem(
|
||||
"suspend", callback=functools.partial(util.cli.execute, suspend_cmd)
|
||||
)
|
||||
menu.add_menuitem(
|
||||
"hibernate", callback=functools.partial(util.cli.execute, hibernate_cmd)
|
||||
)
|
||||
|
||||
menu.show(widget, 0, 0)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
41
bumblebee_status/modules/contrib/taskwarrior.py
Normal file
41
bumblebee_status/modules/contrib/taskwarrior.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
"""Displays the number of pending tasks in TaskWarrior.
|
||||
|
||||
Requires the following library:
|
||||
* taskw
|
||||
|
||||
Parameters:
|
||||
* taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc)
|
||||
|
||||
|
||||
contributed by `chdorb <https://github.com/chdorb>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from taskw import TaskWarrior
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__pending_tasks = "0"
|
||||
|
||||
def update(self):
|
||||
"""Return a string with the number of pending tasks from TaskWarrior."""
|
||||
try:
|
||||
taskrc = self.parameter("taskrc", "~/.taskrc")
|
||||
w = TaskWarrior(config_filename=taskrc)
|
||||
pending_tasks = w.filter_tasks({"status": "pending"})
|
||||
self.__pending_tasks = str(len(pending_tasks))
|
||||
except:
|
||||
self.__pending_tasks = "n/a"
|
||||
|
||||
def output(self, _):
|
||||
"""Format the task counter to output in bumblebee."""
|
||||
return "{}".format(self.__pending_tasks)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
23
bumblebee_status/modules/contrib/timetz.py
Normal file
23
bumblebee_status/modules/contrib/timetz.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current date and time.
|
||||
|
||||
Parameters:
|
||||
* time.format: strftime()-compatible formatting string
|
||||
* time.locale: locale to use rather than the system default
|
||||
"""
|
||||
|
||||
import core.decorators
|
||||
from .datetimetz import Module
|
||||
|
||||
|
||||
class Module(Module):
|
||||
@core.decorators.every(seconds=59) # ensures one update per minute
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme)
|
||||
|
||||
def default_format(self):
|
||||
return "%X %Z"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
86
bumblebee_status/modules/contrib/title.py
Normal file
86
bumblebee_status/modules/contrib/title.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays focused i3 window title.
|
||||
|
||||
Requirements:
|
||||
* i3ipc
|
||||
|
||||
Parameters:
|
||||
* title.max : Maximum character length for title before truncating. Defaults to 64.
|
||||
* title.placeholder : Placeholder text to be placed if title was truncated. Defaults to '...'.
|
||||
* title.scroll : Boolean flag for scrolling title. Defaults to False
|
||||
|
||||
|
||||
contributed by `UltimatePancake <https://github.com/UltimatePancake>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
try:
|
||||
import i3ipc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
no_title = "n/a"
|
||||
|
||||
import core.module
|
||||
import core.decorators
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
# parsing of parameters
|
||||
self.__scroll = util.format.asbool(self.parameter("scroll", False))
|
||||
self.__max = int(self.parameter("max", 64))
|
||||
self.__placeholder = self.parameter("placeholder", "...")
|
||||
self.__title = ""
|
||||
|
||||
# set output of the module
|
||||
self.add_widget(
|
||||
full_text=self.__scrolling_focused_title
|
||||
if self.__scroll
|
||||
else self.__focused_title
|
||||
)
|
||||
|
||||
# create a connection with i3ipc
|
||||
self.__i3 = i3ipc.Connection()
|
||||
# event is called both on focus change and title change
|
||||
self.__i3.on("window", lambda __p_i3, __p_e: self.__pollTitle())
|
||||
# begin listening for events
|
||||
threading.Thread(target=self.__i3.main).start()
|
||||
|
||||
# initialize the first title
|
||||
self.__pollTitle()
|
||||
|
||||
def __focused_title(self, widget):
|
||||
return self.__title
|
||||
|
||||
@core.decorators.scrollable
|
||||
def __scrolling_focused_title(self, widget):
|
||||
return self.__full_title
|
||||
|
||||
def __pollTitle(self):
|
||||
"""Updating current title."""
|
||||
try:
|
||||
self.__full_title = self.__i3.get_tree().find_focused().name
|
||||
except:
|
||||
self.__full_title = no_title
|
||||
if self.__full_title is None:
|
||||
self.__full_title = no_title
|
||||
|
||||
if not self.__scroll:
|
||||
# cut the text if it is too long
|
||||
if len(self.__full_title) > self.__max:
|
||||
self.__title = self.__full_title[
|
||||
0 : self.__max - len(self.__placeholder)
|
||||
]
|
||||
self.__title = "{}{}".format(self.__title, self.__placeholder)
|
||||
else:
|
||||
self.__title = self.__full_title
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
51
bumblebee_status/modules/contrib/todo.py
Normal file
51
bumblebee_status/modules/contrib/todo.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the number of todo items from a text file
|
||||
|
||||
Parameters:
|
||||
* todo.file: File to read TODOs from (defaults to ~/Documents/todo.txt)
|
||||
|
||||
|
||||
contributed by `codingo <https://github.com/codingo>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import os.path
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
|
||||
self.__todos = self.count_items()
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd="xdg-open {}".format(self.__doc)
|
||||
)
|
||||
|
||||
def output(self, widget):
|
||||
return str(self.__todos)
|
||||
|
||||
def update(self):
|
||||
self.__todos = self.count_items()
|
||||
|
||||
def state(self, widgets):
|
||||
if self.__todos == 0:
|
||||
return "empty"
|
||||
return "items"
|
||||
|
||||
def count_items(self):
|
||||
try:
|
||||
i = -1
|
||||
with open(self.__doc) as f:
|
||||
for i, l in enumerate(f):
|
||||
pass
|
||||
return i + 1
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
188
bumblebee_status/modules/contrib/traffic.py
Normal file
188
bumblebee_status/modules/contrib/traffic.py
Normal file
|
@ -0,0 +1,188 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays network IO for interfaces.
|
||||
|
||||
Parameters:
|
||||
* traffic.exclude: Comma-separated list of interface prefixes to exclude (defaults to 'lo,virbr,docker,vboxnet,veth')
|
||||
* traffic.states: Comma-separated list of states to show (prefix with '^' to invert - i.e. ^down -> show all devices that are not in state down)
|
||||
* traffic.showname: If set to False, hide network interface name (defaults to True)
|
||||
* traffic.format: Format string for download/upload speeds.
|
||||
Defaults to '{:.2f}'
|
||||
* traffic.graphlen: Graph lenth in seconds. Positive even integer. Each
|
||||
char shows 2 seconds. If set, enables up/down traffic
|
||||
graphs
|
||||
|
||||
contributed by `meain <https://github.com/meain>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
import psutil
|
||||
import netifaces
|
||||
|
||||
import core.module
|
||||
|
||||
import util.format
|
||||
import util.graph
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self._exclude = tuple(
|
||||
filter(
|
||||
len,
|
||||
util.format.aslist(
|
||||
self.parameter("exclude", "lo,virbr,docker,vboxnet,veth")
|
||||
),
|
||||
)
|
||||
)
|
||||
self._status = ""
|
||||
|
||||
self._showname = util.format.asbool(self.parameter("showname", True))
|
||||
self._format = self.parameter("format", "{:.2f}")
|
||||
self._prev = {}
|
||||
self._states = {}
|
||||
self._lastcheck = 0
|
||||
self._states["include"] = []
|
||||
self._states["exclude"] = []
|
||||
for state in tuple(
|
||||
filter(len, util.format.aslist(self.parameter("states", "")))
|
||||
):
|
||||
if state[0] == "^":
|
||||
self._states["exclude"].append(state[1:])
|
||||
else:
|
||||
self._states["include"].append(state)
|
||||
self._graphlen = int(self.parameter("graphlen", 0))
|
||||
if self._graphlen > 0:
|
||||
self._graphdata = {}
|
||||
self._first_run = True
|
||||
self._update_widgets()
|
||||
|
||||
def state(self, widget):
|
||||
if "traffic.rx" in widget.name:
|
||||
return "rx"
|
||||
if "traffic.tx" in widget.name:
|
||||
return "tx"
|
||||
return self._status
|
||||
|
||||
def update(self):
|
||||
self._update_widgets()
|
||||
|
||||
def create_widget(self, name, txt=None, attributes={}):
|
||||
widget = self.add_widget(name=name, full_text=txt)
|
||||
|
||||
for key in attributes:
|
||||
widget.set(key, attributes[key])
|
||||
|
||||
return widget
|
||||
|
||||
def get_addresses(self, intf):
|
||||
retval = []
|
||||
try:
|
||||
for ip in netifaces.ifaddresses(intf).get(netifaces.AF_INET, []):
|
||||
if ip.get("addr", "") != "":
|
||||
retval.append(ip.get("addr"))
|
||||
except Exception:
|
||||
return []
|
||||
return retval
|
||||
|
||||
def get_minwidth_str(self):
|
||||
"""
|
||||
computes theme.minwidth string
|
||||
based on traffic.format and traffic.graphlen parameters
|
||||
"""
|
||||
minwidth_str = ""
|
||||
if self._graphlen > 0:
|
||||
graph_len = int(self._graphlen / 2)
|
||||
graph_prefix = "0" * graph_len
|
||||
minwidth_str += graph_prefix
|
||||
minwidth_str += "1000"
|
||||
try:
|
||||
length = int(re.match("{:\.(\d+)f}", self._format).group(1))
|
||||
if length > 0:
|
||||
minwidth_str += "." + "0" * length
|
||||
except AttributeError:
|
||||
# return default value
|
||||
return "1000.00KiB/s"
|
||||
finally:
|
||||
minwidth_str += "KiB/s"
|
||||
return minwidth_str
|
||||
|
||||
def _update_widgets(self):
|
||||
interfaces = [
|
||||
i for i in netifaces.interfaces() if not i.startswith(self._exclude)
|
||||
]
|
||||
|
||||
self.clear_widgets()
|
||||
|
||||
counters = psutil.net_io_counters(pernic=True)
|
||||
now = time.time()
|
||||
timediff = now - (self._lastcheck if self._lastcheck else now)
|
||||
if timediff <= 0:
|
||||
timediff = 1
|
||||
self._lastcheck = now
|
||||
for interface in interfaces:
|
||||
if self._graphlen > 0:
|
||||
if interface not in self._graphdata:
|
||||
self._graphdata[interface] = {
|
||||
"rx": [0] * self._graphlen,
|
||||
"tx": [0] * self._graphlen,
|
||||
}
|
||||
if not interface:
|
||||
interface = "lo"
|
||||
state = "down"
|
||||
if len(self.get_addresses(interface)) > 0:
|
||||
state = "up"
|
||||
elif util.format.asbool(self.parameter("hide_down", True)):
|
||||
continue
|
||||
|
||||
if len(self._states["exclude"]) > 0 and state in self._states["exclude"]:
|
||||
continue
|
||||
if (
|
||||
len(self._states["include"]) > 0
|
||||
and state not in self._states["include"]
|
||||
):
|
||||
continue
|
||||
|
||||
data = {
|
||||
"rx": counters[interface].bytes_recv,
|
||||
"tx": counters[interface].bytes_sent,
|
||||
}
|
||||
|
||||
name = "traffic-{}".format(interface)
|
||||
|
||||
if self._showname:
|
||||
self.create_widget(name, interface)
|
||||
|
||||
for direction in ["rx", "tx"]:
|
||||
name = "traffic.{}-{}".format(direction, interface)
|
||||
widget = self.create_widget(
|
||||
name,
|
||||
attributes={"theme.minwidth": self.get_minwidth_str()},
|
||||
)
|
||||
prev = self._prev.get(name, 0)
|
||||
bspeed = (int(data[direction]) - int(prev)) / timediff
|
||||
speed = util.format.byte(bspeed, self._format)
|
||||
txtspeed = "{0}/s".format(speed)
|
||||
if self._graphlen > 0:
|
||||
# skip first value returned by psutil, because it is
|
||||
# giant and ruins the grapth ratio until it gets pushed
|
||||
# out of saved list
|
||||
if self._first_run is True:
|
||||
self._first_run = False
|
||||
else:
|
||||
self._graphdata[interface][direction] = self._graphdata[
|
||||
interface
|
||||
][direction][1:]
|
||||
self._graphdata[interface][direction].append(bspeed)
|
||||
txtspeed = "{}{}".format(
|
||||
util.graph.braille(self._graphdata[interface][direction]),
|
||||
txtspeed,
|
||||
)
|
||||
widget.full_text(txtspeed)
|
||||
self._prev[name] = data[direction]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
43
bumblebee_status/modules/contrib/twmn.py
Normal file
43
bumblebee_status/modules/contrib/twmn.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Toggle twmn notifications.
|
||||
|
||||
contributed by `Pseudonick47 <https://github.com/Pseudonick47>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(""))
|
||||
|
||||
self.__paused = False
|
||||
# Make sure that twmn is currently not paused
|
||||
util.cli.execute("killall -SIGUSR2 twmnd", ignore_errors=True)
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle_status)
|
||||
|
||||
def toggle_status(self, event):
|
||||
self.__paused = not self.__paused
|
||||
|
||||
try:
|
||||
if self.__paused:
|
||||
util.cli.execute("systemctl --user start twmnd")
|
||||
else:
|
||||
util.cli.execute("systemctl --user stop twmnd")
|
||||
except:
|
||||
self.__paused = not self.__paused # toggling failed
|
||||
|
||||
def state(self, widget):
|
||||
if self.__paused:
|
||||
return ["muted"]
|
||||
return ["unmuted"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
28
bumblebee_status/modules/contrib/uptime.py
Normal file
28
bumblebee_status/modules/contrib/uptime.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the system uptime.
|
||||
|
||||
contributed by `ccoors <https://github.com/ccoors>`_ - many thanks!
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
self.__uptime = ""
|
||||
|
||||
def output(self, _):
|
||||
return "{}".format(self.__uptime)
|
||||
|
||||
def update(self):
|
||||
with open("/proc/uptime", "r") as f:
|
||||
uptime_seconds = int(float(f.readline().split()[0]))
|
||||
self.__uptime = timedelta(seconds=uptime_seconds)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
116
bumblebee_status/modules/contrib/vpn.py
Normal file
116
bumblebee_status/modules/contrib/vpn.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
""" Displays the VPN profile that is currently in use.
|
||||
|
||||
Left click opens a popup menu that lists all available VPN profiles and allows to establish
|
||||
a VPN connection using that profile.
|
||||
|
||||
Prerequisites:
|
||||
* tk python library (usually python-tk or python3-tk, depending on your distribution)
|
||||
* nmcli needs to be installed and configured properly.
|
||||
To quickly test, whether nmcli is working correctly, type 'nmcli -g NAME,TYPE,DEVICE con' which
|
||||
lists all the connection profiles that are configured. Make sure that your VPN profile is in that list!
|
||||
|
||||
e.g: to import a openvpn profile via nmcli:
|
||||
`sudo nmcli connection import type openvpn file </path/to/your/openvpn/profile.ovpn>`
|
||||
|
||||
contributed by `bbernhard <https://github.com/bbernhard>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.cli
|
||||
import util.popup
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.vpn_status))
|
||||
|
||||
self.__connected_vpn_profile = None
|
||||
self.__selected_vpn_profile = None
|
||||
|
||||
res = util.cli.execute("nmcli -g NAME,TYPE c")
|
||||
lines = res.splitlines()
|
||||
|
||||
self.__vpn_profiles = []
|
||||
for line in lines:
|
||||
info = line.split(":")
|
||||
try:
|
||||
if self.__isvpn(info[1]):
|
||||
self.__vpn_profiles.append(info[0])
|
||||
except:
|
||||
pass
|
||||
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.popup)
|
||||
|
||||
def __isvpn(self, connection_type):
|
||||
return connection_type in ["vpn", "wireguard"]
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
res = util.cli.execute("nmcli -g NAME,TYPE,DEVICE con")
|
||||
lines = res.splitlines()
|
||||
self.__connected_vpn_profile = None
|
||||
for line in lines:
|
||||
info = line.split(":")
|
||||
if self.__isvpn(info[1]) and info[2] != "":
|
||||
self.__connected_vpn_profile = info[0]
|
||||
|
||||
except Exception as e:
|
||||
logging.exception("Could not get VPN status")
|
||||
self.__connected_vpn_profile = None
|
||||
|
||||
def vpn_status(self, widget):
|
||||
if self.__connected_vpn_profile is None:
|
||||
return "off"
|
||||
return self.__connected_vpn_profile
|
||||
|
||||
def __on_vpndisconnect(self):
|
||||
try:
|
||||
util.cli.execute(
|
||||
"nmcli c down '{vpn}'".format(vpn=self.__connected_vpn_profile)
|
||||
)
|
||||
self.__connected_vpn_profile = None
|
||||
except Exception as e:
|
||||
logging.exception("Could not disconnect VPN connection")
|
||||
|
||||
def __on_vpnconnect(self, name):
|
||||
self.__selected_vpn_profile = name
|
||||
|
||||
try:
|
||||
util.cli.execute(
|
||||
"nmcli c up '{vpn}'".format(vpn=self.__selected_vpn_profile)
|
||||
)
|
||||
self.__connected_vpn_profile = name
|
||||
except Exception as e:
|
||||
logging.exception("Could not establish VPN connection")
|
||||
self.__connected_vpn_profile = None
|
||||
|
||||
def popup(self, widget):
|
||||
menu = util.popup.menu()
|
||||
|
||||
if self.__connected_vpn_profile is not None:
|
||||
menu.add_menuitem("Disconnect", callback=self.__on_vpndisconnect)
|
||||
for vpn_profile in self.__vpn_profiles:
|
||||
if (
|
||||
self.__connected_vpn_profile is not None
|
||||
and self.__connected_vpn_profile == vpn_profile
|
||||
):
|
||||
continue
|
||||
menu.add_menuitem(
|
||||
vpn_profile,
|
||||
callback=functools.partial(self.__on_vpnconnect, vpn_profile),
|
||||
)
|
||||
menu.show(widget)
|
||||
|
||||
def state(self, widget):
|
||||
return []
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
60
bumblebee_status/modules/contrib/watson.py
Normal file
60
bumblebee_status/modules/contrib/watson.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the status of watson (time-tracking tool)
|
||||
|
||||
Requires the following executable:
|
||||
* watson
|
||||
|
||||
contributed by `bendardenne <https://github.com/bendardenne>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
import functools
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.cli
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=60)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.text))
|
||||
|
||||
self.__tracking = False
|
||||
self.__project = ""
|
||||
core.input.register(self, button=core.input.LEFT_MOUSE, cmd=self.toggle)
|
||||
|
||||
def toggle(self, widget):
|
||||
self.__project = "hit"
|
||||
if self.__tracking:
|
||||
util.cli.execute("watson stop")
|
||||
else:
|
||||
util.cli.execute("watson restart")
|
||||
self.__tracking = not self.__tracking
|
||||
|
||||
def text(self, widget):
|
||||
if self.__tracking:
|
||||
return self.__project
|
||||
else:
|
||||
return "Paused"
|
||||
|
||||
def update(self):
|
||||
output = util.cli.execute("watson status")
|
||||
if re.match("No project started", output):
|
||||
self.__tracking = False
|
||||
return
|
||||
|
||||
self.__tracking = True
|
||||
m = re.search(r"Project (.+) started", output)
|
||||
self.__project = m.group(1)
|
||||
|
||||
def state(self, widget):
|
||||
return "on" if self.__tracking else "off"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
147
bumblebee_status/modules/contrib/weather.py
Normal file
147
bumblebee_status/modules/contrib/weather.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the temperature on the current location based on the ip
|
||||
|
||||
Requires the following python packages:
|
||||
* requests
|
||||
|
||||
Parameters:
|
||||
* weather.location: Set location, defaults to 'auto' for getting location automatically from a web service
|
||||
If set to a comma-separated list, left-click and right-click can be used to rotate the locations.
|
||||
Locations should be city names or city ids.
|
||||
* weather.unit: metric (default), kelvin, imperial
|
||||
* weather.showcity: If set to true, show location information, otherwise hide it (defaults to true)
|
||||
* weather.showminmax: If set to true, show the minimum and maximum temperature, otherwise hide it (defaults to false)
|
||||
* weather.apikey: API key from http://api.openweathermap.org
|
||||
|
||||
|
||||
contributed by `TheEdgeOfRage <https://github.com/TheEdgeOfRage>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
|
||||
import util.format
|
||||
import util.location
|
||||
|
||||
import re
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(minutes=15)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.output))
|
||||
|
||||
self.__temperature = 0
|
||||
self.__apikey = self.parameter("apikey", "af7bfe22287c652d032a3064ffa44088")
|
||||
self.__location = util.format.aslist(self.parameter("location", "auto"))
|
||||
|
||||
self.__index = 0
|
||||
self.__showcity = util.format.asbool(self.parameter("showcity", True))
|
||||
self.__showminmax = util.format.asbool(self.parameter("showminmax", False))
|
||||
self.__unit = self.parameter("unit", "metric")
|
||||
self.__valid = False
|
||||
|
||||
core.input.register(
|
||||
self, button=core.input.LEFT_MOUSE, cmd=self.__next_location
|
||||
)
|
||||
core.input.register(
|
||||
self, button=core.input.RIGHT_MOUSE, cmd=self.__prev_location
|
||||
)
|
||||
|
||||
def __next_location(self, event):
|
||||
self.__index = (self.__index + 1) % len(self.__location)
|
||||
self.update()
|
||||
|
||||
def __prev_location(self, event):
|
||||
self.__index = (
|
||||
len(self.__location) - 1 if self.__index <= 0 else self.__index - 1
|
||||
)
|
||||
self.update()
|
||||
|
||||
def temperature(self):
|
||||
return util.format.astemperature(self.__temperature, self.__unit)
|
||||
|
||||
def tempmin(self):
|
||||
return util.format.astemperature(self.__tempmin, self.__unit)
|
||||
|
||||
def tempmax(self):
|
||||
return util.format.astemperature(self.__tempmax, self.__unit)
|
||||
|
||||
def city(self):
|
||||
city = re.sub("[_-]", " ", self.__city)
|
||||
return "{} ".format(city)
|
||||
|
||||
def output(self, widget):
|
||||
if not self.__valid:
|
||||
return "?"
|
||||
if self.__showminmax:
|
||||
self.__showcity = False
|
||||
return (
|
||||
self.city()
|
||||
+ self.temperature()
|
||||
+ " Hi:"
|
||||
+ self.tempmax()
|
||||
+ " Lo:"
|
||||
+ self.tempmin()
|
||||
)
|
||||
elif self.__showcity:
|
||||
return self.city() + self.temperature()
|
||||
else:
|
||||
return self.temperature()
|
||||
|
||||
def state(self, widget):
|
||||
if self.__valid:
|
||||
if "thunderstorm" in self.__weather:
|
||||
return ["thunder"]
|
||||
elif "drizzle" in self.__weather:
|
||||
return ["rain"]
|
||||
elif "rain" in self.__weather:
|
||||
return ["rain"]
|
||||
elif "snow" in self.__weather:
|
||||
return ["snow"]
|
||||
elif "sleet" in self.__weather:
|
||||
return ["sleet"]
|
||||
elif "clear" in self.__weather:
|
||||
return ["clear"]
|
||||
elif "cloud" in self.__weather:
|
||||
return ["clouds"]
|
||||
|
||||
return []
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
weather_url = "http://api.openweathermap.org/data/2.5/weather?appid={}".format(
|
||||
self.__apikey
|
||||
)
|
||||
weather_url = "{}&units={}".format(weather_url, self.__unit)
|
||||
if self.__location[self.__index] == "auto":
|
||||
coord = util.location.coordinates()
|
||||
weather_url = "{url}&lat={lat}&lon={lon}".format(
|
||||
url=weather_url, lat=coord[0], lon=coord[1]
|
||||
)
|
||||
elif self.__location[self.__index].isdigit():
|
||||
weather_url = "{url}&id={id}".format(
|
||||
url=weather_url, id=self.__location[self.__index]
|
||||
)
|
||||
else:
|
||||
weather_url = "{url}&q={city}".format(
|
||||
url=weather_url, city=self.__location[self.__index]
|
||||
)
|
||||
weather = requests.get(weather_url).json()
|
||||
self.__city = weather["name"]
|
||||
self.__temperature = int(weather["main"]["temp"])
|
||||
self.__tempmin = int(weather["main"]["temp_min"])
|
||||
self.__tempmax = int(weather["main"]["temp_max"])
|
||||
self.__weather = weather["weather"][0]["main"].lower()
|
||||
self.__valid = True
|
||||
except Exception:
|
||||
self.__valid = False
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
25
bumblebee_status/modules/contrib/xkcd.py
Normal file
25
bumblebee_status/modules/contrib/xkcd.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Opens a random xkcd comic in the browser.
|
||||
|
||||
contributed by `whzup <https://github.com/whzup>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.input
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.never
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget("xkcd"))
|
||||
core.input.register(
|
||||
self,
|
||||
button=core.input.LEFT_MOUSE,
|
||||
cmd="xdg-open https://c.xkcd.com/random/comic/",
|
||||
)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
41
bumblebee_status/modules/contrib/yubikey.py
Normal file
41
bumblebee_status/modules/contrib/yubikey.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Shows yubikey information
|
||||
|
||||
Requires: https://github.com/Yubico/python-yubico
|
||||
|
||||
The output indicates that a YubiKey is not connected or it displays
|
||||
the corresponding serial number.
|
||||
|
||||
|
||||
contributed by `EmmaTinten <https://github.com/EmmaTinten>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import yubico
|
||||
|
||||
import core.module
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
@core.decorators.every(seconds=5)
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, core.widget.Widget(self.keystate))
|
||||
self.__keystate = "No YubiKey"
|
||||
|
||||
def keystate(self, widget):
|
||||
return self.__keystate
|
||||
|
||||
def update(self):
|
||||
try:
|
||||
self.__keystate = "YubiKey: " + str(
|
||||
yubico.find_yubikey(debug=False).serial()
|
||||
)
|
||||
except yubico.yubico_exception.YubicoError:
|
||||
self.__keystate = "No YubiKey"
|
||||
except Exception:
|
||||
self.__keystate = "n/a"
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
235
bumblebee_status/modules/contrib/zpool.py
Normal file
235
bumblebee_status/modules/contrib/zpool.py
Normal file
|
@ -0,0 +1,235 @@
|
|||
"""Displays info about zpools present on the system
|
||||
|
||||
Parameters:
|
||||
* zpool.list: Comma-separated list of zpools to display info for. If empty, info for all zpools
|
||||
is displayed. (Default: '')
|
||||
* zpool.format: Format string, tags {name}, {used}, {left}, {size}, {percentfree}, {percentuse},
|
||||
{status}, {shortstatus}, {fragpercent}, {deduppercent} are supported.
|
||||
(Default: '{name} {used}/{size} ({percentfree}%)')
|
||||
* zpool.showio: Show also widgets detailing current read and write I/O (Default: true)
|
||||
* zpool.ioformat: Format string for I/O widget, tags {ops} (operations per seconds) and {band}
|
||||
(bandwidth) are supported. (Default: '{band}')
|
||||
* zpool.warnfree: Warn if free space is below this percentage (Default: 10)
|
||||
* zpool.sudo: Use sudo when calling the `zpool` binary. (Default: false)
|
||||
|
||||
Option `zpool.sudo` is intended for Linux users using zfsonlinux older than 0.7.0: In pre-0.7.0
|
||||
releases of zfsonlinux regular users couldn't invoke even informative commands such as
|
||||
`zpool list`. If this option is true, command `zpool list` is invoked with sudo. If this option
|
||||
is used, the following (or ekvivalent) must be added to the `sudoers(5)`:
|
||||
|
||||
```
|
||||
<username/ALL> ALL = (root) NOPASSWD: /usr/bin/zpool list
|
||||
```
|
||||
|
||||
Be aware of security implications of doing this!
|
||||
|
||||
contributed by `adam-dej <https://github.com/adam-dej>`_ - many thanks!
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from pkg_resources import parse_version
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import core.module
|
||||
|
||||
import util.cli
|
||||
import util.format
|
||||
|
||||
|
||||
class Module(core.module.Module):
|
||||
def __init__(self, config, theme):
|
||||
super().__init__(config, theme, [])
|
||||
|
||||
self._includelist = set(
|
||||
filter(
|
||||
lambda x: len(x) > 0,
|
||||
util.format.aslist(self.parameter("list", default="")),
|
||||
)
|
||||
)
|
||||
self._format = self.parameter(
|
||||
"format", default="{name} {shortstatus} {used}/{size} " + "({percentfree}%)"
|
||||
)
|
||||
self._usesudo = util.format.asbool(self.parameter("sudo", default=False))
|
||||
self._showio = util.format.asbool(self.parameter("showio", default=True))
|
||||
self._ioformat = self.parameter("ioformat", default="{band}")
|
||||
self._warnfree = int(self.parameter("warnfree", default=10))
|
||||
|
||||
def update(self):
|
||||
self.clear_widgets()
|
||||
zfs_version_path = "/sys/module/zfs/version"
|
||||
# zpool list -H: List all zpools, use script mode (no headers and tabs as separators).
|
||||
try:
|
||||
with open(zfs_version_path, "r") as zfs_mod_version:
|
||||
zfs_version = zfs_mod_version.readline().rstrip().split("-")[0]
|
||||
except IOError:
|
||||
# ZFS isn't installed or the module isn't loaded, stub the version
|
||||
zfs_version = "0.0.0"
|
||||
logging.error(
|
||||
"ZFS version information not found at {}, check the module is loaded.".format(
|
||||
zfs_version_path
|
||||
)
|
||||
)
|
||||
|
||||
raw_zpools = util.cli.execute(
|
||||
("sudo " if self._usesudo else "") + "zpool list -H"
|
||||
).split("\n")
|
||||
|
||||
for raw_zpool in raw_zpools:
|
||||
try:
|
||||
# Ignored fields (assigned to _) are 'expandsz' and 'altroot', also 'ckpoint' in ZFS 0.8.0+
|
||||
if parse_version(zfs_version) < parse_version("0.8.0"):
|
||||
(
|
||||
name,
|
||||
size,
|
||||
alloc,
|
||||
free,
|
||||
_,
|
||||
frag,
|
||||
cap,
|
||||
dedup,
|
||||
health,
|
||||
_,
|
||||
) = raw_zpool.split("\t")
|
||||
else:
|
||||
(
|
||||
name,
|
||||
size,
|
||||
alloc,
|
||||
free,
|
||||
_,
|
||||
_,
|
||||
frag,
|
||||
cap,
|
||||
dedup,
|
||||
health,
|
||||
_,
|
||||
) = raw_zpool.split("\t")
|
||||
cap = cap.rstrip("%")
|
||||
percentuse = int(cap)
|
||||
percentfree = 100 - percentuse
|
||||
# There is a command, zpool iostat, which is however blocking and was therefore
|
||||
# causing issues.
|
||||
# Instead, we read file `/proc/spl/kstat/zfs/<poolname>/io` which contains
|
||||
# cumulative I/O statistics since boot (or pool creation). We store these values
|
||||
# (and timestamp) during each widget update, and during the next widget update we
|
||||
# use them to compute delta of transferred bytes, and using the last and current
|
||||
# timestamp the rate at which they have been transferred.
|
||||
with open("/proc/spl/kstat/zfs/{}/io".format(name), "r") as f:
|
||||
# Third row provides data we need, we are interested in the first 4 values.
|
||||
# More info about this file can be found here:
|
||||
# https://github.com/zfsonlinux/zfs/blob/master/lib/libspl/include/sys/kstat.h#L580
|
||||
# The 4 values are:
|
||||
# nread, nwritten, reads, writes
|
||||
iostat = list(map(int, f.readlines()[2].split()[:4]))
|
||||
except (ValueError, IOError):
|
||||
# Unable to parse info about this pool, skip it
|
||||
continue
|
||||
|
||||
if self._includelist and name not in self._includelist:
|
||||
continue
|
||||
|
||||
widget = self.widget(name)
|
||||
if not widget:
|
||||
widget = self.add_widget(name=name)
|
||||
widget.set("last_iostat", [0, 0, 0, 0])
|
||||
widget.set("last_timestamp", 0)
|
||||
|
||||
delta_iostat = [b - a for a, b in zip(iostat, widget.get("last_iostat"))]
|
||||
widget.set("last_iostat", iostat)
|
||||
|
||||
# From docs:
|
||||
# > Note that even though the time is always returned as a floating point number, not
|
||||
# > all systems provide time with a better precision than 1 second.
|
||||
# Be aware that that may affect the precision of reported I/O
|
||||
# Also, during one update cycle the reported I/O may be garbage if the system time
|
||||
# was changed.
|
||||
timestamp = time.time()
|
||||
delta_timestamp = widget.get("last_timestamp") - timestamp
|
||||
widget.set("last_timestamp", time.time())
|
||||
|
||||
# abs is there because sometimes the result is -0
|
||||
rate_iostat = [abs(x / delta_timestamp) for x in delta_iostat]
|
||||
nread, nwritten, reads, writes = rate_iostat
|
||||
|
||||
# theme.minwidth is not set since these values are not expected to change
|
||||
# rapidly
|
||||
widget.full_text(
|
||||
self._format.format(
|
||||
name=name,
|
||||
used=alloc,
|
||||
left=free,
|
||||
size=size,
|
||||
percentfree=percentfree,
|
||||
percentuse=percentuse,
|
||||
status=health,
|
||||
shortstatus=self._shortstatus(health),
|
||||
fragpercent=frag,
|
||||
deduppercent=dedup,
|
||||
)
|
||||
)
|
||||
widget.set("state", health)
|
||||
widget.set("percentfree", percentfree)
|
||||
widget.set("visited", True)
|
||||
|
||||
if self._showio:
|
||||
wname, rname = [name + x for x in ["__write", "__read"]]
|
||||
widget_w = self.widget(wname)
|
||||
widget_r = self.widget(rname)
|
||||
if not widget_w or not widget_r:
|
||||
widget_r = self.add_widget(name=rname)
|
||||
widget_w = self.add_widget(name=wname)
|
||||
for w in [widget_r, widget_w]:
|
||||
w.set(
|
||||
"theme.minwidth",
|
||||
self._ioformat.format(
|
||||
ops=9999, band=util.format.bytefmt(999.99 * (1024 ** 2))
|
||||
),
|
||||
)
|
||||
widget_w.full_text(
|
||||
self._ioformat.format(
|
||||
ops=round(writes), band=util.format.bytefmt(nwritten)
|
||||
)
|
||||
)
|
||||
widget_r.full_text(
|
||||
self._ioformat.format(
|
||||
ops=round(reads), band=util.format.bytefmt(nread)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def state(self, widget):
|
||||
if widget.name.endswith("__read"):
|
||||
return "poolread"
|
||||
elif widget.name.endswith("__write"):
|
||||
return "poolwrite"
|
||||
|
||||
state = widget.get("state")
|
||||
if state == "FAULTED":
|
||||
return [state, "critical"]
|
||||
elif state == "DEGRADED" or widget.get("percentfree") < self._warnfree:
|
||||
return [state, "warning"]
|
||||
|
||||
return state
|
||||
|
||||
@staticmethod
|
||||
def _shortstatus(status):
|
||||
# From `zpool(8)`, section Device Failure and Recovery:
|
||||
# A pool's health status is described by one of three states: online, degraded, or faulted.
|
||||
# An online pool has all devices operating normally. A degraded pool is one in which one
|
||||
# or more devices have failed, but the data is still available due to a redundant
|
||||
# configuration. A faulted pool has corrupted metadata, or one or more faulted devices, and
|
||||
# insufficient replicas to continue functioning.
|
||||
shortstate = {
|
||||
"DEGRADED": "DEG",
|
||||
"FAULTED": "FLT",
|
||||
"ONLINE": "ONL",
|
||||
}
|
||||
try:
|
||||
return shortstate[status]
|
||||
except KeyError:
|
||||
return ""
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
0
bumblebee_status/modules/core/__init__.py
Normal file
0
bumblebee_status/modules/core/__init__.py
Normal file
41
bumblebee_status/modules/core/cpu.py
Normal file
41
bumblebee_status/modules/core/cpu.py
Normal 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
|
23
bumblebee_status/modules/core/date.py
Normal file
23
bumblebee_status/modules/core/date.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# pylint: disable=C0111,R0903
|
||||
|
||||
"""Displays the current date and time.
|
||||
|
||||
Parameters:
|
||||
* date.format: strftime()-compatible formatting string
|
||||
* date.locale: locale to use rather than the system default
|
||||
"""
|
||||
|
||||
import core.decorators
|
||||
from .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
|
45
bumblebee_status/modules/core/datetime.py
Normal file
45
bumblebee_status/modules/core/datetime.py
Normal 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
|
24
bumblebee_status/modules/core/debug.py
Normal file
24
bumblebee_status/modules/core/debug.py
Normal 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
|
65
bumblebee_status/modules/core/disk.py
Normal file
65
bumblebee_status/modules/core/disk.py
Normal 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
|
33
bumblebee_status/modules/core/error.py
Normal file
33
bumblebee_status/modules/core/error.py
Normal 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
|
77
bumblebee_status/modules/core/git.py
Normal file
77
bumblebee_status/modules/core/git.py
Normal 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
|
71
bumblebee_status/modules/core/layout-xkb.py
Normal file
71
bumblebee_status/modules/core/layout-xkb.py
Normal 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
|
41
bumblebee_status/modules/core/load.py
Normal file
41
bumblebee_status/modules/core/load.py
Normal 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
|
77
bumblebee_status/modules/core/memory.py
Normal file
77
bumblebee_status/modules/core/memory.py
Normal 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
|
135
bumblebee_status/modules/core/nic.py
Normal file
135
bumblebee_status/modules/core/nic.py
Normal 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
|
9
bumblebee_status/modules/core/pasink.py
Normal file
9
bumblebee_status/modules/core/pasink.py
Normal 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
|
9
bumblebee_status/modules/core/pasource.py
Normal file
9
bumblebee_status/modules/core/pasource.py
Normal 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
|
95
bumblebee_status/modules/core/ping.py
Normal file
95
bumblebee_status/modules/core/ping.py
Normal 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
|
200
bumblebee_status/modules/core/pulseaudio.py
Normal file
200
bumblebee_status/modules/core/pulseaudio.py
Normal 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
|
113
bumblebee_status/modules/core/redshift.py
Normal file
113
bumblebee_status/modules/core/redshift.py
Normal 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
|
242
bumblebee_status/modules/core/sensors2.py
Normal file
242
bumblebee_status/modules/core/sensors2.py
Normal 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
|
24
bumblebee_status/modules/core/spacer.py
Normal file
24
bumblebee_status/modules/core/spacer.py
Normal 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
|
15
bumblebee_status/modules/core/test.py
Normal file
15
bumblebee_status/modules/core/test.py
Normal 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
Loading…
Add table
Add a link
Reference in a new issue