[core] Rework core implementation

Experimental re-implementation of core functionality with the aim:
- Depend only on the Python Standard Library for core
- If modules are missing elsewhere, *never* throw
- Unit test *everything*
- Cleaner and more minimal implementation
- Better integration points for existing implementations (charts,
  braille, etc.)
- Full backwards-compatibility with existing module system (except where
  modules can be vastly simplified)
This commit is contained in:
Tobias Witek 2020-01-19 13:29:34 +01:00
parent 72f88b3409
commit e931bb93c6
214 changed files with 45 additions and 13040 deletions

View file

@ -1,7 +0,0 @@
#start with a Python 3.x container
FROM python:3
#grab repository from github
RUN git clone --recursive https://github.com/tobi-wan-kenobi/bumblebee-status.git /var/bumblebee-status
#run the statusline with no modules or themes specified
CMD python3 /var/bumblebee-status/bumblebee-status

View file

@ -1,18 +0,0 @@
DEBIAN_ROOT = build/debian/bumblebee-status/
deb:
mkdir -p $(DEBIAN_ROOT)/DEBIAN
mkdir -p $(DEBIAN_ROOT)/usr/share/bumblebee-status/modules
mkdir -p $(DEBIAN_ROOT)/usr/share/bumblebee-status/themes
cp build/debian/control $(DEBIAN_ROOT)/DEBIAN/
cp bumblebee-status $(DEBIAN_ROOT)/usr/share/bumblebee-status/
cp bumblebee/modules/*.py $(DEBIAN_ROOT)/usr/share/bumblebee-status/modules/
cp -r themes/* $(DEBIAN_ROOT)/usr/share/bumblebee-status/themes
cp LICENSE $(DEBIAN_ROOT)/usr/share/bumblebee-status/
dpkg-deb --build $(DEBIAN_ROOT)
cp build/debian/bumblebee-status.deb .
clean:
rm -rf $(DEBIAN_ROOT)
rm -f build/debian/*.deb
rm -f *.deb

View file

@ -1,30 +0,0 @@
#!/usr/bin/env bash
if [ ! -f ~/.config/i3/config.template ]; then
cp ~/.config/i3/config ~/.config/i3/config.template
else
cp ~/.config/i3/config.template ~/.config/i3/config
fi
if [ -f ~/.config/i3/config.template.private ]; then
cat ~/.config/i3/config.template.private >> ~/.config/i3/config
fi
screens=$(xrandr -q|grep ' connected'| grep -P '\d+x\d+' |cut -d' ' -f1)
echo "screens: $screens"
while read -r line; do
screen=$(echo $line | cut -d' ' -f1)
others=$(echo $screens|tr ' ' '\n'|grep -v $screen|tr '\n' '-'|sed 's/.$//')
if [ -f ~/.config/i3/config.$screen-$others ]; then
cat ~/.config/i3/config.$screen-$others >> ~/.config/i3/config
else
if [ -f ~/.config/i3/config.$screen ]; then
cat ~/.config/i3/config.$screen >> ~/.config/i3/config
fi
fi
done <<< "$screens"
i3-msg restart

View file

@ -1,22 +0,0 @@
#!/usr/bin/bash
if ! type -P fakeroot >/dev/null; then
error 'Cannot find the fakeroot binary.'
exit 1
fi
if [[ -z $CHECKUPDATES_DB ]]; then
CHECKUPDATES_DB="${TMPDIR:-/tmp}/checkup-db-${USER}/"
fi
trap 'rm -f $CHECKUPDATES_DB/db.lck' INT TERM EXIT
DBPath="${DBPath:-/var/lib/pacman/}"
eval $(awk -F' *= *' '$1 ~ /DBPath/ { print $1 "=" $2 }' /etc/pacman.conf)
mkdir -p "$CHECKUPDATES_DB"
ln -s "${DBPath}/local" "$CHECKUPDATES_DB" &> /dev/null
fakeroot -- pacman -Sy --dbpath "$CHECKUPDATES_DB" --logfile /dev/null &> /dev/null
fakeroot pacman -Su -p --dbpath "$CHECKUPDATES_DB"
exit 0

View file

@ -1,15 +0,0 @@
#!/usr/bin/env bash
echo $(dirname $(readlink -f "$0"))
i3bar_update=$(dirname $(readlink -f "$0"))/load-i3-bars.sh
xrandr "$@"
if [ -f $i3bar_update ]; then
sleep 1
if [ -f ~/.config/i3/images/background.png ]; then
feh --bg-fill ~/.config/i3/images/background.png
fi
$i3bar_update
fi

View file

@ -1,5 +0,0 @@
Package: bumblebee-status
Version: 0
Maintainer: tobi-wan-kenobi
Architecture: all
Description: bumblebee-status is a modular, theme-able status line generator for the i3 window manager.

View file

@ -1,5 +0,0 @@
#!/bin/sh
ln -s /usr/share/bumblebee-status/bumblebee-status /usr/local/bin/bumblebee-status

View file

@ -1,80 +1,21 @@
#!/usr/bin/env python #!/usr/bin/env python
import os
import sys import sys
import logging import core.output
import signal
import bumblebee.theme
import bumblebee.engine
import bumblebee.config
import bumblebee.output
import bumblebee.input
import bumblebee.modules.error
try:
reload(sys)
sys.setdefaultencoding('UTF8')
except Exception:
pass
def main(): def main():
def sig_USR1_handler(signum,stack): output = core.output.i3()
engine.write_output() modules = []
# modules = core.module.modules()
config = bumblebee.config.Config(sys.argv[1:]) sys.stdout.write(output.start())
if config.debug():
if config.logfile() in ["stdout", "stderr"]:
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] %(levelname)-8s %(message)s",
stream=sys.stdout if config.logfile() == "stdout" else sys.stderr
)
else:
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] %(levelname)-8s %(message)s",
filename=config.logfile()
)
theme = bumblebee.theme.Theme(config.theme(), config.iconset())
output = bumblebee.output.I3BarOutput(theme=theme, config=config)
inp = bumblebee.input.I3BarInput()
engine = None
try:
engine = bumblebee.engine.Engine(
config=config,
output=output,
inp=inp,
theme=theme,
)
signal.signal(10,sig_USR1_handler)
engine.run()
except KeyboardInterrupt as error:
inp.stop()
sys.exit(0)
except BaseException as e:
if not engine: raise
module = engine.current_module()
logging.exception(e)
if output.started():
output.flush()
output.end()
else:
output.start()
import time
while True: while True:
output.begin() sys.stdout.write(output.begin_status_line())
error = bumblebee.modules.error.Module(engine, config) for module in modules:
error.set("exception occurred: {} in {}".format(e, module)) # module.update()
widget = error.widgets()[0] sys.stdout.write(output.draw(module))
widget.link_module(error) sys.stdout.write(output.end_status_line())
output.draw(widget, error) sys.stdout.write(output.stop())
output.flush()
output.end()
time.sleep(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

View file

@ -1,132 +0,0 @@
"""Configuration handling
This module provides configuration information (loaded modules,
module parameters, etc.) to all other components
"""
import os
import sys
import logging
import argparse
import textwrap
import importlib
import bumblebee.store
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."
THEME_HELP = "Specify the theme to use for drawing modules"
PARAMETER_HELP = "Provide configuration parameters in the form of <module>.<key>=<value>"
LIST_HELP = "Display a list of either available themes or available modules along with their parameters."
DEBUG_HELP = "Enable debug log, This will create '~/bumblebee-status-debug.log' by default, can be changed with the '-f' option"
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.print_modules()
elif value == "themes":
self.print_themes()
sys.exit(0)
def print_themes(self):
print(", ".join(bumblebee.theme.themes()))
def print_modules(self):
if self._args.list_format == "markdown":
print("# Table of modules")
print("|Name |Description |")
print("|-----|------------|")
for m in bumblebee.engine.all_modules():
try:
mod = importlib.import_module("bumblebee.modules.{}".format(m["name"]))
if self._args.list_format == "markdown":
print("|{} |{} |".format(m["name"], mod.__doc__.replace("\n", "<br>")))
else:
print(textwrap.fill("{}:".format(m["name"]), 80,
initial_indent=self._indent*2, subsequent_indent=self._indent*2))
for line in mod.__doc__.split("\n"):
print(textwrap.fill(line, 80,
initial_indent=self._indent*3, subsequent_indent=self._indent*6))
except:
pass
def create_parser():
"""Create the argument parser"""
parser = argparse.ArgumentParser(description="display system data in the i3bar")
parser.add_argument("-m", "--modules", nargs="+", action='append', default=[],
help=MODULE_HELP)
parser.add_argument("-t", "--theme", default="default", help=THEME_HELP)
parser.add_argument("--markup", default="none", help="Specify the markup type of the output (e.g. 'pango')")
parser.add_argument("-p", "--parameters", nargs="+", action='append', default=[],
help=PARAMETER_HELP)
parser.add_argument("-l", "--list", choices=["modules", "themes"], action=print_usage,
help=LIST_HELP)
parser.add_argument("--list-format", choices=["plain", "markdown"], help="output format of -l, *must* be specified before -l")
parser.add_argument("-d", "--debug", action="store_true",
help=DEBUG_HELP)
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("-f", "--logfile", default="~/bumblebee-status-debug.log",
help="Location of the debug log file")
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")
return parser
class Config(bumblebee.store.Store):
"""Top-level configuration class
Parses commandline arguments and provides non-module
specific configuration information.
"""
def __init__(self, args=None):
super(Config, self).__init__()
parser = create_parser()
self._args = parser.parse_args(args if args else [])
if not self._args.debug:
logging.getLogger().disabled = True
parameters = [item for sub in self._args.parameters for item in sub]
for param in parameters:
key, value = param.split("=", 1)
self.set(key, value)
def modules(self):
modules = [item for sub in self._args.modules for item in sub]
"""Return a list of all activated modules"""
return [{
"module": x.split(":")[0],
"name": x if not ":" in x else x.split(":")[1],
} for x in modules]
def theme(self):
"""Return the name of the selected theme"""
return self._args.theme
def iconset(self):
"""Return the name of a user-specified icon-set"""
return self._args.iconset
def debug(self):
return self._args.debug
def reverse(self):
return self._args.right_to_left
def logfile(self):
return os.path.expanduser(self._args.logfile)
def autohide(self):
return self._args.autohide
def markup(self):
return self._args.markup
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,306 +0,0 @@
"""Core application engine"""
import os
import json
import time
import pkgutil
import logging
import importlib
import bumblebee.error
import bumblebee.modules
log = logging.getLogger(__name__)
try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
def all_modules():
"""Return a list of available modules"""
result = []
path = os.path.dirname(bumblebee.modules.__file__)
for mod in [name for _, name, _ in pkgutil.iter_modules([path])]:
result.append({
"name": mod
})
return result
class Module(object):
"""Module instance base class
Objects of this type represent the modules that
the user configures. Concrete module implementations
(e.g. CPU utilization, disk usage, etc.) derive from
this base class.
"""
def __init__(self, engine, config={}, widgets=[]):
self.name = config.get("name", self.__module__.split(".")[-1])
self._config = config
self.id = self.name
self.error = None
self._next = int(time.time())
self._default_interval = 0
self._interval_factor = 1
self._engine = engine
self._configFile = None
for cfg in [os.path.expanduser("~/.bumblebee-status.conf"), os.path.expanduser("~/.config/bumblebee-status.conf")]:
if os.path.exists(cfg):
self._configFile = RawConfigParser()
self._configFile.read(cfg)
log.debug("reading configuration file {}".format(cfg))
break
if self._configFile is not None and self._configFile.has_section("module-parameters"):
log.debug(self._configFile.items("module-parameters"))
self._widgets = []
if widgets:
self._widgets = widgets if isinstance(widgets, list) else [widgets]
def theme(self):
return self._engine.theme()
def widgets(self, widgets=None):
"""Return the widgets to draw for this module"""
if widgets:
self._widgets = widgets if isinstance(widgets, list) else [widgets]
return self._widgets
def hidden(self):
return False
def widget(self, name):
for widget in self._widgets:
if widget.name == name:
return widget
def errorWidget(self):
msg = self.error
if len(msg) > 10:
msg = "{}...".format(msg[0:7])
return bumblebee.output.Widget(full_text="error: {}".format(msg))
def widget_by_id(self, uid):
for widget in self._widgets:
if widget.id == uid:
return widget
return None
def update(self, widgets):
"""By default, update() is a NOP"""
pass
def update_wrapper(self, widgets):
if self._next > int(time.time()):
return
try:
self.error = None
self.update(self._widgets)
except Exception as e:
log.error("error updating '{}': {}".format(self.name, str(e)))
self.error = str(e)
self._next += int(self.parameter("interval", self._default_interval))*self._interval_factor
def interval_factor(self, factor):
self._interval_factor = factor
def interval(self, intvl):
self._default_interval = intvl
self._next = int(time.time())
def update_all(self):
self.update_wrapper(self._widgets)
def has_parameter(self, name):
v = self.parameter(name)
return v is not None
def parameter(self, name, default=None):
"""Return the config parameter 'name' for this module"""
module_name = "{}.{}".format(self.__module__.split(".")[-1], name)
name = "{}.{}".format(self.name, name)
value = self._config["config"].get(module_name, default)
value = self._config["config"].get(name, value)
if value == default:
try:
value = self._configFile.get("module-parameters", name)
except:
pass
return value
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 Engine(object):
"""Engine for driving the application
This class connects input/output, instantiates all
required modules and drives the "event loop"
"""
def __init__(self, config, output=None, inp=None, theme=None):
self._output = output
self._config = config
self._running = True
self._modules = []
self.input = inp
self._aliases = self._aliases()
self.load_modules(config.modules())
self._current_module = None
self._theme = theme
if bumblebee.util.asbool(config.get("engine.workspacewheel", "true")):
if bumblebee.util.asbool(config.get("engine.workspacewrap", "true")):
self.input.register_callback(None, bumblebee.input.WHEEL_UP,
"i3-msg workspace prev_on_output")
self.input.register_callback(None, bumblebee.input.WHEEL_DOWN,
"i3-msg workspace next_on_output")
else:
self.input.register_callback(None, bumblebee.input.WHEEL_UP,
cmd=self._prev_workspace)
self.input.register_callback(None, bumblebee.input.WHEEL_DOWN,
cmd=self._next_workspace)
if bumblebee.util.asbool(config.get("engine.collapsible", "true")):
self.input.register_callback(None, bumblebee.input.MIDDLE_MOUSE,
cmd=self._toggle_minimize)
self.input.start()
def theme(self):
return self._theme
def _toggle_minimize(self, event):
for module in self._modules:
widget = module.widget_by_id(event["instance"])
if widget:
log.debug("module {} found - toggle minimize".format(module.id))
widget.toggle_minimize()
def _prev_workspace(self, event):
self._change_workspace(-1)
def _next_workspace(self, event):
self._change_workspace(1)
def _change_workspace(self, amount):
try:
active_output = None
active_index = -1
output_workspaces = {}
data = json.loads(bumblebee.util.execute("i3-msg -t get_workspaces"))
for workspace in data:
output_workspaces.setdefault(workspace["output"], []).append(workspace)
if workspace["focused"]:
active_output = workspace["output"]
active_index = len(output_workspaces[workspace["output"]]) - 1
if (active_index + amount) < 0:
return
if (active_index + amount) >= len(output_workspaces[active_output]):
return
while amount != 0:
if amount > 0:
bumblebee.util.execute("i3-msg workspace next_on_output")
amount = amount - 1
if amount < 0:
bumblebee.util.execute("i3-msg workspace prev_on_output")
amount = amount + 1
except Exception as e:
log.error("failed to change workspace: {}".format(e))
def modules(self):
return self._modules
def load_modules(self, modules):
"""Load specified modules and return them as list"""
for module in modules:
mod = self._load_module(module["module"], module["name"])
self._modules.append(mod)
self._register_module_callbacks(mod)
return self._modules
def _register_module_callbacks(self, module):
buttons = [
{"name": "left-click", "id": bumblebee.input.LEFT_MOUSE},
{"name": "middle-click", "id": bumblebee.input.MIDDLE_MOUSE},
{"name": "right-click", "id": bumblebee.input.RIGHT_MOUSE},
{"name": "wheel-up", "id": bumblebee.input.WHEEL_UP},
{"name": "wheel-down", "id": bumblebee.input.WHEEL_DOWN},
]
for button in buttons:
if module.parameter(button["name"], None):
self.input.register_callback(obj=module,
button=button["id"], cmd=module.parameter(button["name"]))
def _aliases(self):
return {
'date': 'datetime',
'time': 'datetime',
'datetz': 'datetimetz',
'timetz': 'datetimetz',
'pasink': 'pulseaudio',
'pasource': 'pulseaudio',
'test-alias': 'test',
}
def _load_module(self, module_name, config_name=None):
"""Load specified module and return it as object"""
if module_name in self._aliases:
config_name is config_name if config_name else module_name
module_name = self._aliases[module_name]
if config_name is None:
config_name = module_name
err = None
try:
module = importlib.import_module("bumblebee.modules.{}".format(module_name))
except ImportError as error:
err = error
log.fatal("failed to import {}: {}".format(module_name, str(error)))
if err:
raise bumblebee.error.ModuleLoadError("unable to load module {}: {}".format(module_name, str(err)))
return getattr(module, "Module")(self, {
"name": config_name,
"config": self._config
})
def running(self):
"""Check whether the event loop is running"""
return self._running
def stop(self):
"""Stop the event loop"""
self._running = False
def current_module(self):
return self._current_module.__module__
def run(self):
"""Start the event loop"""
self._output.start()
while self.running():
self.write_output()
if self.running():
self.input.wait(float(self._config.get("interval", 1)))
self._output.stop()
self.input.stop()
def write_output(self):
self._output.begin()
for module in self._modules:
self._current_module = module
module.update_wrapper(module.widgets())
if module.error is None:
for widget in module.widgets():
widget.link_module(module)
self._output.draw(widget=widget, module=module, engine=self)
else:
self._output.draw(widget=module.errorWidget(), module=module, engine=self)
self._output.flush()
self._output.end()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,15 +0,0 @@
"""Collection of all exceptions raised by this tool"""
class BaseError(Exception):
"""Base class for all exceptions generated by this tool"""
pass
class ModuleLoadError(BaseError):
"""Raised whenever loading a module fails"""
pass
class ThemeLoadError(BaseError):
"""Raised whenever loading a theme fails"""
pass
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,149 +0,0 @@
"""Input classes"""
import sys
import json
import uuid
import time
import select
import logging
import threading
import bumblebee.util
log = logging.getLogger(__name__)
LEFT_MOUSE = 1
MIDDLE_MOUSE = 2
RIGHT_MOUSE = 3
WHEEL_UP = 4
WHEEL_DOWN = 5
def is_terminated():
for thread in threading.enumerate():
if thread.name == "MainThread" and not thread.is_alive():
return True
return False
def read_input(inp):
"""Read i3bar input and execute callbacks"""
poll = select.poll()
poll.register(sys.stdin.fileno(), select.POLLIN)
log.debug("starting click event processing")
while inp.running:
if is_terminated():
return
try:
events = poll.poll(1000)
except Exception:
continue
for fileno, event in events:
line = "["
while line.startswith("["):
line = sys.stdin.readline().strip(",").strip()
log.debug("new event: {}".format(line))
inp.has_event = True
try:
event = json.loads(line)
if "instance" in event:
inp.callback(event)
inp.redraw()
else:
log.debug("field 'instance' missing in input, not processing the event")
except ValueError as e:
log.debug("failed to parse event: {}".format(e))
log.debug("exiting click event processing")
poll.unregister(sys.stdin.fileno())
inp.has_event = True
inp.clean_exit = True
class I3BarInput(object):
"""Process incoming events from the i3bar"""
def __init__(self):
self.running = True
self._callbacks = {}
self.clean_exit = False
self.global_id = str(uuid.uuid4())
self.need_event = False
self.has_event = False
self._condition = threading.Condition()
def start(self):
"""Start asynchronous input processing"""
self.has_event = False
self.running = True
self._condition.acquire()
self._thread = threading.Thread(target=read_input, args=(self,))
self._thread.start()
def redraw(self):
self._condition.acquire()
self._condition.notify()
self._condition.release()
def alive(self):
"""Check whether the input processing is still active"""
return self._thread.is_alive()
def wait(self, timeout):
self._condition.wait(timeout)
def _wait(self):
while not self.has_event:
time.sleep(0.1)
self.has_event = False
def stop(self):
"""Stop asynchronous input processing"""
self._condition.release()
if self.need_event:
self._wait()
self.running = False
self._thread.join()
return self.clean_exit
def _uuidstr(self, name, button):
return "{}::{}".format(name, button)
def _uid(self, obj, button):
uid = self.global_id
if obj:
uid = obj.id
return self._uuidstr(uid, button)
def deregister_callbacks(self, obj):
to_delete = []
uid = obj.id if obj else self.global_id
for key in self._callbacks:
if uid in key:
to_delete.append(key)
for key in to_delete:
del self._callbacks[key]
def register_callback(self, obj, button, cmd):
"""Register a callback function or system call"""
uid = self._uid(obj, button)
if uid not in self._callbacks:
self._callbacks[uid] = {}
self._callbacks[uid] = cmd
def callback(self, event):
"""Execute callback action for an incoming event"""
button = event["button"]
cmd = self._callbacks.get(self._uuidstr(self.global_id, button), None)
cmd = self._callbacks.get(self._uuidstr(event["name"], button), cmd)
cmd = self._callbacks.get(self._uuidstr(event["instance"], button), cmd)
if cmd is None:
return
try:
if callable(cmd):
cmd(event)
else:
bumblebee.util.execute(cmd, False)
except Exception:
# fall back to global default
if not "__fallback" in event:
return self.callback({"name": None, "instance": None, "__fallback": True, "button": event["button"]})
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,42 +0,0 @@
"""get volume level
"""
import re
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.volume)
)
self._level = "0"
self._muted = True
device = self.parameter("device", "Master,0")
self._cmdString = "amixer get {}".format(device)
def volume(self, widget):
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, widgets):
level = ""
try:
level = bumblebee.util.execute(self._cmdString)
except Exception as e:
level = ""
self._level = level
def state(self, widget):
if self._muted:
return ["warning", "muted"]
return ["unmuted"]

View file

@ -1,81 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays APT package update information (<to upgrade>/<to remove >)
Requires the following debian packages:
* python-parse
* aptitude
"""
import threading
from parse import *
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
APT_CHECK_PATH = ("aptitude full-upgrade --simulate --assume-yes")
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 = parse(PATTERN, line_to_parse)
return int(result[0]), int(result[2])
def get_apt_check_info(widget):
try:
res = bumblebee.util.execute(APT_CHECK_PATH)
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)
except e:
widget.set("error", "parse error: {}".format(e))
return
widget.set("to_upgrade", to_upgrade)
widget.set("to_remove", to_remove)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.updates)
super(Module, self).__init__(engine, config, widget)
self.interval_factor(60)
self.interval(30)
def updates(self, widget):
result = []
if widget.get("error"):
return widget.get("error")
for t in ["to_upgrade", "to_remove"]:
result.append(str(widget.get(t, 0)))
return "/".join(result)
def update(self, widgets):
thread = threading.Thread(target=get_apt_check_info, args=(widgets[0],))
thread.start()
def state(self, widget):
cnt = 0
ret = "good"
for t in ["to_upgrade", "to_remove"]:
cnt += widget.get(t, 0)
if cnt > 50:
ret = "critical"
elif cnt > 0:
ret = "warning"
if widget.get("error"):
ret = "critical"
return ret
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,47 +0,0 @@
"""Check updates to Arch Linux.
"""
import subprocess
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.utilization)
super(Module, self).__init__(engine, config, widget)
self.packages = self.check_updates()
def check_updates(self):
p = subprocess.Popen(
"checkupdates", stdout=subprocess.PIPE, shell=True)
p_status = p.wait()
if p_status == 0:
(output, err) = p.communicate()
output = output.decode('utf-8')
packages = output.split('\n')
packages.pop()
return len(packages)
return 0
@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.check_updates() == 0
def update(self, widgets):
self.packages = self.check_updates()
def state(self, widget):
return self.threshold_state(self.packages, 1, 100)

View file

@ -1,270 +0,0 @@
# 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
"""
import dbus
import logging
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
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(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=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)))
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="gnome-power-statistics")
self._showremaining = bumblebee.util.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 = bumblebee.util.durationfmt(remain, shorten=True, suffix=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")
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

View file

@ -1,140 +0,0 @@
# 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)
"""
import os
import glob
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
try:
import power
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
super(Module, self).__init__(engine, config, widgets)
self._batteries = self.parameter("device", "auto").split(",")
if self._batteries[0] == "auto":
self._batteries = glob.glob("/sys/class/power_supply/BAT*")
else:
self._batteries = ["/sys/class/power_supply/{}".format(b) for b in self._batteries]
if len(self._batteries) == 0:
self._batteries = ["/sys/class/power_supply/BAT0"]
self.update(widgets)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="gnome-power-statistics")
def update(self, widgets):
new_widgets = []
for path in self._batteries:
widget = self.widget(path)
if not widget:
widget = bumblebee.output.Widget(full_text=self.capacity, name=path)
new_widgets.append(widget)
self.capacity(widget)
while len(widgets) > 0: del widgets[0]
for widget in new_widgets:
if bumblebee.util.asbool(self.parameter("decorate", True)) == False:
widget.set("theme.exclude", "suffix")
widgets.append(widget)
self._widgets = widgets
def remaining(self):
estimate = 0.0
try:
estimate = power.PowerManagement().get_time_remaining_estimate()
# do not show remaining if on AC
if estimate == power.common.TIME_REMAINING_UNLIMITED:
return None
if estimate == power.common.TIME_REMAINING_UNKNOWN:
return ""
except Exception:
return ""
return bumblebee.util.durationfmt(estimate*60, shorten=True, suffix=True) # estimate is in minutes
def capacity(self, widget):
widget.set("capacity", -1)
widget.set("ac", False)
if not os.path.exists(widget.name):
widget.set("capacity", 100)
widget.set("ac", True)
return "ac"
capacity = 100
try:
with open("{}/capacity".format(widget.name)) as f:
capacity = int(f.read())
except IOError:
return "n/a"
capacity = capacity if capacity < 100 else 100
widget.set("capacity", capacity)
# Read power conumption
if bumblebee.util.asbool(self.parameter("showpowerconsumption", False)):
r=open(widget.name + '/power_now', "r")
output = "{}% ({})".format(capacity,str(int(r.read())/1000000) + "W")
else:
output = "{}%".format(capacity)
widget.set("theme.minwidth", "100%")
if bumblebee.util.asbool(self.parameter("showremaining", True))\
and self.getCharge(widget) == "Discharging":
output = "{} {}".format(output, self.remaining())
if bumblebee.util.asbool(self.parameter("showdevice", False)):
output = "{} ({})".format(output, os.path.basename(widget.name))
return output
def state(self, widget):
state = []
capacity = widget.get("capacity")
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 = self.getCharge(widget)
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
def getCharge(self, widget):
charge = ""
try:
with open("{}/status".format(widget.name)) as f:
charge = f.read().strip()
except IOError:
pass
return charge
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,130 +0,0 @@
# 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)
* batter.showremaining : If set to true (default) shows the remaining time until the batteries are completely discharged
"""
import os
import glob
import logging
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
try:
import power
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
self._batteries = []
try:
for battery in os.listdir('/sys/class/power_supply/'):
if not any(i in battery for i in ['AC', 'hidpp']):
self._batteries.append("/sys/class/power_supply/" + battery)
except Exception as e:
logging.exception("unable to detect batteries: {}".format(str(e)))
super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.capacity))
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="gnome-power-statistics")
def remaining(self):
estimate = 0.0
power_now = 0.0
try:
estimate = power.PowerManagement().get_time_remaining_estimate()
# do not show remaining if on AC
if estimate == power.common.TIME_REMAINING_UNLIMITED:
return None
elif estimate == power.common.TIME_REMAINING_UNKNOWN:
return ""
except Exception:
return ""
return bumblebee.util.durationfmt(estimate*60, shorten=True, suffix=True) # estimate is in minutes
def capacity(self, widget):
widget.set("capacity", -1)
widget.set("ac", False)
capacity = 100
self.energy_now = 0
self.energy_full = 0
errors = 0
for path in self._batteries:
try:
with open("{}/energy_full".format(path)) as f:
self.energy_full += int(f.read())
with open("{}/energy_now".format(path)) as o:
self.energy_now += int(o.read())
except IOError:
return "n/a"
except Exception:
errors += 1
if errors == len(self._batteries):
return "n/a"
capacity = int( float(self.energy_now) / float(self.energy_full) * 100.0)
capacity = capacity if capacity < 100 else 100
widget.set("capacity", capacity)
output = "{}%".format(capacity)
widget.set("theme.minwidth", "100%")
if bumblebee.util.asbool(self.parameter("showremaining", True))\
and self.getCharge(widget) == "Discharging":
output = "{} {}".format(output, self.remaining())
return output
def state(self, widget):
state = []
capacity = widget.get("capacity")
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 = self.getCharge(widget)
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
def getCharge(self, widget):
charge = ""
charge_list = []
for x in range(len(self._batteries)):
try:
with open("{}/status".format(self._batteries[x])) as f:
charge_list.append(f.read().strip())
except IOError:
pass
for x in range(len(charge_list)):
if charge_list[x] == "Discharging":
charge = charge_list[x]
break
return charge

View file

@ -1,131 +0,0 @@
"""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)
"""
import os
import re
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
import bumblebee.popup
import logging
class Module(bumblebee.engine.Module):
"""Bluetooth module."""
def __init__(self, engine, config):
"""Initialize."""
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(
full_text=self.status))
device = self.parameter("device", "hci0")
self.manager = self.parameter("manager", "blueman-manager")
self._path = "/sys/class/bluetooth/{}".format(device)
self._status = "Off"
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.manager)
# determine whether to use pop-up menu or simply toggle the device on/off
right_click_popup = bumblebee.util.asbool(
self.parameter("right_click_popup", True))
if right_click_popup:
engine.input.register_callback(self,
button=bumblebee.input.RIGHT_MOUSE,
cmd=self.popup)
else:
engine.input.register_callback(self,
button=bumblebee.input.RIGHT_MOUSE,
cmd=self._toggle)
def status(self, widget):
"""Get status."""
return self._status
def update(self, widgets):
"""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 manager(self, widget):
"""Launch manager."""
bumblebee.util.execute(self.manager)
def popup(self, widget):
"""Show a popup menu."""
menu = bumblebee.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')
bumblebee.util.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

View file

@ -1,63 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays the brightness of a display
Parameters:
* brightness.step: The amount of increase/decrease on scroll in % (defaults to 2)
* brightness.device_path: The device path (defaults to /sys/class/backlight/intel_backlight), can contain wildcards (in this case, the first matching path will be used)
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import glob
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.brightness))
self._brightness = 0
self._device_path = self.find_device(self.parameter("device_path", "/sys/class/backlight/intel_backlight"))
step = self.parameter("step", 2)
if bumblebee.util.which("light"):
self.register_cmd(engine, "light -A {}%".format(step),
"light -U {}%".format(step))
elif bumblebee.util.which("brightnessctl"):
self.register_cmd(engine, "brightnessctl s {}%+".format(step),
"brightnessctl s {}%-".format(step))
else:
self.register_cmd(engine, "xbacklight +{}%".format(step),
"xbacklight -{}%".format(step))
def find_device(self, device_path):
res = glob.glob(device_path)
if len(res) == 0:
return device_path
return res[0]
def register_cmd(self, engine, upCmd, downCmd):
engine.input.register_callback(self, button=bumblebee.input.WHEEL_UP, cmd=upCmd)
engine.input.register_callback(self, button=bumblebee.input.WHEEL_DOWN, cmd=downCmd)
def brightness(self, widget):
if isinstance(self._brightness, float):
return "{:3.0f}%".format(self._brightness).strip()
else:
return "n/a"
def update(self, widgets):
try:
with open("{}/brightness".format(self._device_path)) as f:
backlight = int(f.readline())
with open("{}/max_brightness".format(self._device_path)) as f:
max_brightness = int(f.readline())
self._brightness = float(backlight * 100 / max_brightness)
except:
return "Error"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,102 +0,0 @@
#pylint: disable=C0111,R0903,W0212
"""Enable/disable automatic screen locking.
Requires the following executables:
* xdg-screensaver
* xdotool
* xprop (as dependency for xdotool)
* notify-send
"""
import logging
import os
import psutil
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="")
)
self._active = False
self._xid = None
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self._toggle
)
def _check_requirements(self):
requirements = ['xdotool', 'xprop', 'xdg-screensaver']
missing = []
for tool in requirements:
if not bumblebee.util.which(tool):
missing.append(tool)
return missing
def _get_i3bar_xid(self):
xid = bumblebee.util.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 bumblebee.util.which('notify-send'):
return
if self._active:
bumblebee.util.execute("notify-send \"Consuming caffeine\"")
else:
bumblebee.util.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()
bumblebee.util.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 = bumblebee.util.which('xprop')
pids = [ p.pid for p in psutil.process_iter() if p.cmdline() == [xprop_path, '-id', str(self._xid), '-spy'] ]
for pid in pids:
try:
os.kill(pid, 9)
except OSError:
success = False
return success
def state(self, _):
if self._active:
return "activated"
return "deactivated"
def _toggle(self, _):
missing = self._check_requirements()
if missing:
logging.warning('Could not run caffeine - missing %s!', ", ".join(missing))
return
self._active = not self._active
if self._active:
success = self._suspend_screensaver()
else:
success = self._resume_screensaver()
if success:
self._notify()
else:
self._active = not self._active
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,120 +0,0 @@
# 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.
"""
from collections import defaultdict
import os
import string
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, [])
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._status = None
self._shuffle = False
self._repeat = False
self._tags = defaultdict(lambda: '')
# Create widgets
widget_list = []
widget_map = {}
for widget_name in self._layout.split():
widget = bumblebee.output.Widget(name=widget_name)
widget_list.append(widget)
if widget_name == "cmus.prev":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "cmus-remote -r"}
elif widget_name == "cmus.main":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "cmus-remote -u"}
widget.full_text(self.description)
elif widget_name == "cmus.next":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "cmus-remote -n"}
elif widget_name == "cmus.shuffle":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "cmus-remote -S"}
elif widget_name == "cmus.repeat":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "cmus-remote -R"}
else:
raise KeyError("The cmus module does not support a {widget_name!r} widget".format(widget_name=widget_name))
self.widgets(widget_list)
# Register input callbacks
for widget, callback_options in widget_map.items():
engine.input.register_callback(widget, **callback_options)
def hidden(self):
return self._status is None
@scrollable
def description(self, widget):
return string.Formatter().vformat(self._fmt, (), self._tags)
def update(self, widgets):
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: bumblebee.util.durationfmt(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 = bumblebee.util.execute("cmus-remote -Q")
except RuntimeError:
self._status = None
self._tags = defaultdict(lambda: '')
for line in info.split("\n"):
self._eval_line(line)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,42 +0,0 @@
# 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}%")
"""
try:
import psutil
except ImportError:
pass
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.utilization)
super(Module, self).__init__(engine, config, widget)
widget.set("theme.minwidth", self._format.format(10.0-10e-20))
self._utilization = psutil.cpu_percent(percpu=False)
engine.input.register_callback(self, button=bumblebee.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, widgets):
self._utilization = psutil.cpu_percent(percpu=False)
def state(self, _):
return self.threshold_state(self._utilization, 70, 80)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,157 +0,0 @@
"""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)
if this is set to 1, use --markup=pango
* 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.
"""
try:
import psutil
except ImportError:
pass
import bumblebee.engine
import bumblebee.util
import bumblebee.output
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, [])
self._layout = self.parameter("layout", "cpu2.maxfreq cpu2.cpuload cpu2.coresload cpu2.temp cpu2.fanspeed")
self._widget_names = self._layout.split()
widget_list = []
for widget_name in self._widget_names:
if widget_name == "cpu2.maxfreq":
widget = bumblebee.output.Widget(
name=widget_name, full_text=self.maxfreq)
widget.set("type", "freq")
elif widget_name == "cpu2.cpuload":
widget = bumblebee.output.Widget(
name=widget_name, full_text=self.cpuload)
widget.set("type", "load")
elif widget_name == "cpu2.coresload":
widget = bumblebee.output.Widget(
name=widget_name, full_text=self.coresload)
widget.set("type", "loads")
elif widget_name == "cpu2.temp":
widget = bumblebee.output.Widget(
name=widget_name, full_text=self.temp)
widget.set("type", "temp")
elif widget_name == "cpu2.fanspeed":
widget = bumblebee.output.Widget(
name=widget_name, full_text=self.fanspeed)
widget.set("type", "fan")
widget_list.append(widget)
self.widgets(widget_list)
self._colored = bumblebee.util.asbool(self.parameter("colored", 0))
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
self.update(widget_list)
def maxfreq(self, _):
return "{:.2f}GHz".format(self._maxfreq)
def cpuload(self, _):
return "{}%".format(self._cpuload)
@staticmethod
def add_color(bar):
"""add color as pango markup to a bar"""
if bar in ["", ""]:
color = "green"
elif bar in ["", ""]:
color = "yellow"
elif bar in ["", ""]:
color = "orange"
elif bar in ["", ""]:
color = "red"
colored_bar = "<span foreground='{}'>{}</span>".format(color, bar)
return colored_bar
def coresload(self, _):
mono_bars = [bumblebee.output.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 = bumblebee.util.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", "")]

View file

@ -1,139 +0,0 @@
# -*- 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
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
try:
import requests
except ImportError:
pass
try:
from babel.numbers import format_currency
except ImportError:
format_currency = None
import json
import os
SYMBOL = {
"GBP": u"£", "EUR": u"", "USD": u"$", "JPY": u"¥", "KRW": u""
}
DEFAULT_DEST = "USD,EUR,auto"
DEFAULT_SRC = "GBP"
API_URL = "https://markets.ft.com/data/currencies/ajax/conversion?baseCurrency={}&comparison={}"
LOCATION_URL = "https://ipvigilante.com/"
def get_local_country():
r = requests.get(LOCATION_URL)
location = r.json()
return location['data']['country_name']
def load_country_to_currency():
fname = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'data', 'country-by-currency-code.json')
with open(fname, 'r') as f:
data = json.load(f)
country2curr = {}
for dt in data:
country2curr[dt['country']] = dt['currency_code']
return country2curr
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.price)
)
self._data = []
self.interval_factor(60)
self.interval(1)
self._nextcheck = 0
src = self.parameter("source", DEFAULT_SRC)
if src == "auto":
self._base = self.find_local_currency()
else:
self._base = src
self._symbols = []
for d in self.parameter("destination", DEFAULT_DEST).split(","):
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(u"{}{}".format(rate, SYMBOL[sym] if sym in SYMBOL else sym))
basefmt = u"{}".format(self.parameter("sourceformat", "{}={}"))
ratefmt = u"{}".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, widgets):
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 = get_local_country()
currency_map = load_country_to_currency()
return currency_map.get(country, DEFAULT_SRC)
except:
return DEFAULT_SRC
def fmt_rate(self, rate):
float_rate = float(rate.replace(',', ''))
if not 0.01 < float_rate < 100:
ret = rate
else:
ret = "%.3g" % float_rate
return ret
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,974 +0,0 @@
[
{
"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"
}
]

View file

@ -1,50 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays the current date and time.
Parameters:
* datetime.format: strftime()-compatible formatting string
* date.format : alias for datetime.format
* time.format : alias for datetime.format
* datetime.locale: locale to use rather than the system default
* date.locale : alias for datetime.locale
* time.locale : alias for datetime.locale
"""
from __future__ import absolute_import
import datetime
import locale
import bumblebee.engine
def default_format(module):
default = "%x %X"
if module == "date":
default = "%x"
if module == "time":
default = "%X"
return default
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.get_time))
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="calendar")
self._fmt = self.parameter("format", default_format(self.name))
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 get_time(self, widget):
enc = locale.getpreferredencoding()
retval = datetime.datetime.now().strftime(self._fmt)
if hasattr(retval, "decode"):
return retval.decode(enc)
return retval
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,93 +0,0 @@
# 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
"""
from __future__ import absolute_import
import datetime
import locale
import logging
try:
import pytz
import tzlocal
except:
pass
import bumblebee.input
import bumblebee.output
import bumblebee.engine
def default_format(module):
default = "%x %X %Z"
if module == "datetz":
default = "%x %Z"
if module == "timetz":
default = "%X %Z"
return default
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.get_time))
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self.next_tz)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self.prev_tz)
self._fmt = self.parameter("format", default_format(self.name))
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 = self.parameter("timezone", default_timezone).split(",")
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 get_time(self, widget):
try:
try:
tz = pytz.timezone(self._timezones[self._current_tz].strip())
retval = datetime.datetime.now(tz=tzlocal.get_localzone()).astimezone(tz).strftime(self._fmt)
except pytz.exceptions.UnknownTimeZoneError:
retval = "[Unknown timezone: {}]".format(self._timezones[self._current_tz].strip())
except Exception as e:
logging.error('unable to get time: {}'.format(str(e)))
retval = "[n/a]"
enc = locale.getpreferredencoding()
if hasattr(retval, "decode"):
return retval.decode(enc)
return retval
def next_tz(self, event):
next_timezone = self._current_tz + 1
if next_timezone >= len(self._timezones):
next_timezone = 0 # wraparound
self._current_tz = next_timezone
def prev_tz(self, event):
previous_timezone = self._current_tz - 1
if previous_timezone < 0:
previous_timezone = 0 # wraparound
self._current_tz = previous_timezone
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,143 +0,0 @@
# 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
"""
import sys
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
try:
import subprocess
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.deadbeef)
)
buttons = {"LEFT_CLICK": bumblebee.input.LEFT_MOUSE,
"RIGHT_CLICK": bumblebee.input.RIGHT_MOUSE,
"MIDDLE_CLICK": bumblebee.input.MIDDLE_MOUSE,
"SCROLL_UP": bumblebee.input.WHEEL_UP,
"SCROLL_DOWN": bumblebee.input.WHEEL_DOWN,
}
self._song = ""
self._format = self.parameter("format", "{artist} - {title}")
self._tf_format = self.parameter("tf_format", "")
self._show_tf_when_stopped = bool(self.parameter("tf_format_if_stopped", ""))
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 "
engine.input.register_callback(self, button=buttons[prev_button],
cmd=cmd + "--prev")
engine.input.register_callback(self, button=buttons[next_button],
cmd=cmd + "--next")
engine.input.register_callback(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)
@scrollable
def deadbeef(self, widget):
return self.string_song
def hidden(self):
return self.string_song == ""
def update(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:
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 read_process(self.now_playing) == "nothing":
self._song = ""
return
## perform the actual query -- these can be much more sophisticated
self.now_playing_tf[-1] = self._tf_format
data = read_process(self.now_playing_tf)
self._song = data
def update_standard(self, widgets):
data = read_process(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)
def read_process(command):
proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
return proc.stdout.read()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,77 +0,0 @@
# 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
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
try:
import dbus
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.deezer)
)
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE,
"RIGHT_CLICK":bumblebee.input.RIGHT_MOUSE,
"MIDDLE_CLICK":bumblebee.input.MIDDLE_MOUSE,
"SCROLL_UP":bumblebee.input.WHEEL_UP,
"SCROLL_DOWN":bumblebee.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."
engine.input.register_callback(self, button=buttons[prev_button],
cmd=cmd + "Previous")
engine.input.register_callback(self, button=buttons[next_button],
cmd=cmd + "Next")
engine.input.register_callback(self, button=buttons[pause_button],
cmd=cmd + "PlayPause")
## @scrollable
def deezer(self, widget):
return str(self._song)
def hidden(self):
return str(self._song) == ""
def update(self, widgets):
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=u"\u25B6" if playback_status=="Playing" else u"\u258D\u258D" if playback_status=="Paused" else "",)
except Exception:
self._song = ""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,88 +0,0 @@
# 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}%)")
* (deprecated) disk.showUsed: Show used space (defaults to yes)
* (deprecated) disk.showSize: Show total size (defaults to yes)
* (deprecated) disk.showPercent: Show usage percentage (defaults to yes)
"""
import os
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.diskspace)
)
self._path = self.parameter("path", "/")
self._format = self.parameter("format", "{used}/{size} ({percent:05.02f}%)")
self._app = self.parameter("open", "xdg-open")
self._used = 0
self._left = 0
self._size = 0
self._percent = 0
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="{} {}".format(self._app,
self._path))
def diskspace(self, widget):
used_str = bumblebee.util.bytefmt(self._used)
size_str = bumblebee.util.bytefmt(self._size)
left_str = bumblebee.util.bytefmt(self._left)
percent_str = self._percent
sused = self.has_parameter("showUsed")
ssize = self.has_parameter("showSize")
spercent = self.has_parameter("showPercent")
if all(not param for param in (sused, ssize, spercent)):
return self._format.format(path=self._path,
used=used_str,
left=left_str,
size=size_str,
percent=percent_str)
else:
rv = ""
sused = bumblebee.util.asbool(self.parameter("showUsed", True))
ssize = bumblebee.util.asbool(self.parameter("showSize", True))
spercent = bumblebee.util.asbool(self.parameter("showPercent", True))
if sused:
rv = "{}{}".format(rv, used_str)
if sused and ssize:
rv = "{}/".format(rv)
if ssize:
rv = "{}{}".format(rv, size_str)
if spercent:
if not sused and not ssize:
rv = "{:05.02f}%".format(percent_str)
else:
rv = "{} ({:05.02f}%)".format(rv, percent_str)
return rv
def update(self, widgets):
st = os.statvfs(self._path)
self._size = st.f_blocks * st.f_frsize
self._used = (st.f_blocks - st.f_bfree) * st.f_frsize
self._left = self._size - self._used;
self._percent = 100.0 * self._used/self._size
def state(self, widget):
return self.threshold_state(self._percent, 80, 90)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,77 +0,0 @@
# 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 bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
def get_dnf_info(widget):
try:
res = bumblebee.util.execute("dnf updateinfo")
except RuntimeError:
pass
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)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.updates)
super(Module, self).__init__(engine, config, widget)
self.interval_factor(60)
self.interval(30)
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, widgets):
thread = threading.Thread(target=get_dnf_info, args=(widgets[0],))
thread.start()
def state(self, widget):
cnt = 0
for t in ["security", "bugfixes", "enhancements", "other"]:
cnt += widget.get(t, 0)
if cnt == 0:
return "good"
if cnt > 50 or widget.get("security", 0) > 0:
return "critical"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,39 +0,0 @@
# -*- coding: utf-8 -*-
"""Displays the number of docker containers running
Requires the following python packages:
* docker
"""
try:
import docker
except ImportError:
pass
from requests.exceptions import ConnectionError
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.status)
super(Module, self).__init__(engine, config, widget)
self._status = self.status
def update(self, widgets):
self._status = self.status
def status(self, _):
try:
cli = docker.DockerClient(base_url='unix://var/run/docker.sock')
cli.ping()
except ConnectionError:
return "Daemon off"
except Exception:
return "n/a"
return "OK - {}".format(len(cli.containers.list(filters={'status': "running"})))

View file

@ -1,39 +0,0 @@
#pylint: disable=C0111,R0903
"""Toggle dunst notifications."""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="")
)
self._paused = False
# Make sure that dunst is currently not paused
try:
bumblebee.util.execute("killall -SIGUSR2 dunst")
except:
pass
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.toggle_status
)
def toggle_status(self, event):
self._paused = not self._paused
try:
if self._paused:
bumblebee.util.execute("killall -SIGUSR1 dunst")
else:
bumblebee.util.execute("killall -SIGUSR2 dunst")
except:
self._paused = not self._paused # toggling failed
def state(self, widget):
if self._paused:
return ["muted", "warning"]
return ["unmuted"]

View file

@ -1,26 +0,0 @@
# pylint: disable=C0111,R0903
"""Draws an error widget.
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._text = ""
def set(self, text):
self._text = text
def text(self, widget):
return self._text
def state(self, widget):
return ["critical"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,75 +0,0 @@
# 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.
"""
import requests
import time
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from requests.exceptions import RequestException
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(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.curprice)
)
self._curprice = ""
self._nextcheck = 0
self._interval = int(self.parameter("interval", "120"))
self._getbtc = bumblebee.util.asbool(self.parameter("getbtc", True))
self._geteth = bumblebee.util.asbool(self.parameter("geteth", True))
self._getltc = bumblebee.util.asbool(self.parameter("getltc", True))
self._getcur = self.parameter("getcur", "usd")
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="xdg-open https://cryptowat.ch/")
def curprice(self, widget):
return self._curprice
def update(self, widgets):
if self._nextcheck < int(time.time()):
self._nextcheck = int(time.time()) + self._interval
currency = self._getcur
btcprice, ethprice, ltcprice = "", "", ""
if self._getbtc:
btcprice = getfromkrak('Btc', currency)
if self._geteth:
ethprice = getfromkrak('Eth', currency)
if self._getltc:
ltcprice = getfromkrak('Ltc', currency)
self._curprice = btcprice+" "*(self._getbtc*self._geteth)+ethprice+" "*(self._getltc*max(self._getbtc, self._geteth))+ltcprice
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,81 +0,0 @@
# pylint: disable=C0111,R0903
"""Print the branch and git status for the
currently focused window.
Requires:
* xcwd
* Python module 'pygit2'
"""
import os
import string
import logging
try:
import pygit2
except ImportError:
pass
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
super(Module, self).__init__(engine, config, widgets)
self._engine = engine
self._error = False
self.update(self.widgets())
def hidden(self):
return self._error
def update(self, widgets):
state = {}
new_widgets = []
try:
directory = bumblebee.util.execute("xcwd").strip()
directory = self._get_git_root(directory)
repo = pygit2.Repository(directory)
new_widgets.append(bumblebee.output.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:
new_widgets.append(bumblebee.output.Widget(name='git.new'))
if 'modified' in state:
new_widgets.append(bumblebee.output.Widget(name='git.modified'))
if 'deleted' in state:
new_widgets.append(bumblebee.output.Widget(name='git.deleted'))
while len(widgets) > 0:
del widgets[0]
for widget in new_widgets:
widgets.append(widget)
except Exception as e:
self._error = True
def state(self, widget):
return widget.name.split('.')[1]
def _get_git_root(self, directory):
while len(directory) > 1:
if os.path.exists(os.path.join(directory, ".git")):
return directory
directory = "/".join(directory.split("/")[0:-1])
return "/"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,56 +0,0 @@
# 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.
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
try:
import requests
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.github)
)
self._count = 0
self.interval_factor(60)
self.interval(5)
self._requests = requests.Session()
self._requests.headers.update({"Authorization":"token {}".format(self.parameter("token", ""))})
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="x-www-browser https://github.com/notifications")
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self.update)
def github(self, _):
return str(self._count)
def update(self, _):
try:
self._count = 0
url = "https://api.github.com/notifications"
while True:
notifications = self._requests.get(url)
self._count += len(list(filter(lambda notification: notification['unread'], notifications.json())))
next_link = notifications.links.get('next')
if next_link is not None:
url = next_link.get('url')
else:
break
except Exception:
self._count = "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,56 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays information about the current song in Google Play music player.
Requires the following executable:
* gpmdp-remote
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = [
bumblebee.output.Widget(name="gpmdp.prev"),
bumblebee.output.Widget(name="gpmdp.main", full_text=self.description),
bumblebee.output.Widget(name="gpmdp.next"),
]
super(Module, self).__init__(engine, config, widgets)
engine.input.register_callback(widgets[0], button=bumblebee.input.LEFT_MOUSE,
cmd="playerctl previous")
engine.input.register_callback(widgets[1], button=bumblebee.input.LEFT_MOUSE,
cmd="playerctl play-pause")
engine.input.register_callback(widgets[2], button=bumblebee.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, widgets):
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 = ""
try:
info = bumblebee.util.execute("gpmdp-remote current")
status = bumblebee.util.execute("gpmdp-remote status")
except RuntimeError:
pass
self._status = status.split("\n")[0].lower()
self._tags = info.split("\n")[0]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,90 +0,0 @@
# -*- coding: utf-8 -*-
"""Fetch hard drive temeperature data from a hddtemp daemon
that runs on localhost and default port (7634)
"""
import socket
import bumblebee.engine
import bumblebee.output
HOST = "localhost"
PORT = 7634
CHUNK_SIZE = 1024
RECORD_SIZE = 5
SEPARATOR = "|"
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.hddtemps)
super(Module, self).__init__(engine, config, widget)
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()

View file

@ -1,24 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays the system hostname."""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._hname = ""
def output(self, _):
return self._hname+" "+u"\uf233"
def update(self, widgets):
with open('/proc/sys/kernel/hostname', 'r') as f:
self._hname = f.readline().split()[0]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,68 +0,0 @@
# 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
"""
from requests import head
import psutil
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
UNK = "UNK"
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.output)
super(Module, self).__init__(engine, config, widget)
self._label = self.parameter("label")
self._target = self.parameter("target")
self._expect = self.parameter("expect", "200")
self._status = self.getStatus()
self._output = self.getOutput()
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:
return self.UNK
else:
status = str(res.status_code)
self._status = status
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, widgets):
self.getStatus()
self._output = self.getOutput()
def state(self, widget):
if self._status == self.UNK:
return "warning"
if self._status != self._expect:
return "critical"
return self._output
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,51 +0,0 @@
#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")
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
self.status = False
super(Module,self).__init__(engine, config, widgets)
self._include = tuple(filter(len, self.parameter("include", "NumLock,CapsLock").split(",")))
self._signalType = self.parameter("signaltype") if not self.parameter("signaltype") is None else "warning"
def update(self, widgets):
self._update_widgets(widgets)
def state(self, widget):
states = []
if widget.status:
states.append(self._signalType)
elif not widget.status:
states.append("normal")
return states
def _update_widgets(self, widgets):
status_line = ""
for line in bumblebee.util.execute("xset q").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 = bumblebee.output.Widget(name=indicator)
widgets.append(widget)
widget.status = True if indicator.lower()+":on" in status_line.lower() else False
widget.full_text(indicator)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,21 +0,0 @@
# pylint: disable=C0111,R0903
"""Shows Linux kernel version information"""
import platform
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._release_name = platform.release()
def output(self, widget):
return self._release_name
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,65 +0,0 @@
# 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.
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
has_xkb = True
try:
from xkbgroup import *
except ImportError:
has_xkb = False
import logging
log = logging.getLogger(__name__)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.current_layout)
)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self._next_keymap)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE,
cmd=self._prev_keymap)
self._show_variant = bumblebee.util.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):
if not has_xkb: return
xkb = XKeyboard()
if xkb.groups_count < 2: return # nothing to doA
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 bumblebee.util.asbool(self.parameter("showname")) else xkb.group_symbol
if self._show_variant:
return "{} ({})".format(name, xkb.group_variant) if xkb.group_variant else name
return name
except Exception:
return "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,40 +0,0 @@
"""Displays and changes the current keyboard layout
Requires the following executable:
* xkb-switch
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.current_layout)
super(Module, self).__init__(engine, config, widget)
engine.input.register_callback(
self,
button=bumblebee.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):
try:
bumblebee.util.execute("xkb-switch -n")
except RuntimeError:
pass
def _get_current_layout(self):
try:
res = bumblebee.util.execute("xkb-switch")
return res.split("\n")[0]
except RuntimeError:
return ["n/a"]
def update(self, __):
self._current_layout = self._get_current_layout()

View file

@ -1,72 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays and changes the current keyboard layout
Requires the following executable:
* setxkbmap
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.current_layout)
)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self._next_keymap)
engine.input.register_callback(self, button=bumblebee.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 "")
try:
bumblebee.util.execute("setxkbmap -layout {} -variant {}".format(",".join(layout_list), ",".join(variant_list)))
except RuntimeError:
pass
def get_layouts(self):
try:
res = bumblebee.util.execute("setxkbmap -query")
except RuntimeError:
return ["n/a"]
layouts = []
variants = []
for line in res.split("\n"):
if not line: continue
if "layout" in line:
layouts = line.split(":")[1].strip().split(",")
if "variant" in line:
variants = line.split(":")[1].strip().split(",")
result = []
for idx, layout in enumerate(layouts):
if len(variants) > idx and variants[idx]:
layout = "{}:{}".format(layout, variants[idx])
result.append(layout)
return result if len(result) > 0 else ["n/a"]
def current_layout(self, widget):
layouts = self.get_layouts()
return layouts[0]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,41 +0,0 @@
# 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 bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.load)
)
self._load = [0, 0, 0]
try:
self._cpus = multiprocessing.cpu_count()
except NotImplementedError as e:
self._cpus = 1
engine.input.register_callback(self, button=bumblebee.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, widgets):
self._load = os.getloadavg()
def state(self, widget):
return self.threshold_state(self._load[0], self._cpus*0.7, self._cpus*0.8)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,72 +0,0 @@
# 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 bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Container(object):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.memory_usage)
)
self.update(None)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="gnome-system-monitor")
@property
def _format(self):
if bumblebee.util.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, widgets):
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": bumblebee.util.bytefmt(data["MemTotal"]),
"available": bumblebee.util.bytefmt(data["MemAvailable"]),
"free": bumblebee.util.bytefmt(data["MemFree"]),
"used": bumblebee.util.bytefmt(used),
"percent": float(used)/float(data["MemTotal"])*100.0
}
def state(self, widget):
if self._mem["percent"] > float(self.parameter("critical", 90)):
return "critical"
if self._mem["percent"] > float(self.parameter("warning", 80)):
return "warning"
return None
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,60 +0,0 @@
# 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
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(name="mocp.main", full_text=self.description)
)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="mocp -G")
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE,
cmd="mocp -t shuffle")
self._format = self.parameter("format", "%state %artist - %song | %ct/%tt")
self._running = 0
#@scrollable
def description(self, widget):
return self._info if self._running == 1 else "Music On Console Player"
def update(self, widgets):
self._load_song()
def _load_song(self):
try:
self._info = bumblebee.util.execute("mocp -Q '" + self._format + "'" ).strip()
self._running = 1
except RuntimeError:
self._running = 0
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,181 +0,0 @@
# 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.
"""
from collections import defaultdict
import string
import os
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, [])
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_list = []
widget_map = {}
for widget_name in self._layout.split():
widget = bumblebee.output.Widget(name=widget_name)
widget_list.append(widget)
if widget_name == "mpd.prev":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "mpc prev" + self._hostcmd}
elif widget_name == "mpd.main":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "mpc toggle" + self._hostcmd}
widget.full_text(self.description)
elif widget_name == "mpd.next":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "mpc next" + self._hostcmd}
elif widget_name == "mpd.shuffle":
widget_map[widget] = {"button": bumblebee.input.LEFT_MOUSE, "cmd": "mpc random" + self._hostcmd}
elif widget_name == "mpd.repeat":
widget_map[widget] = {"button": bumblebee.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))
self.widgets(widget_list)
# Register input callbacks
for widget, callback_options in widget_map.items():
engine.input.register_callback(widget, **callback_options)
def hidden(self):
return self._status is None
@scrollable
def description(self, widget):
return string.Formatter().vformat(self._fmt, (), self._tags)
def update(self, widgets):
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 = ""
try:
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 = bumblebee.util.execute('mpc -f ' + '"' + joinedtags + '"' + self._hostcmd)
except RuntimeError:
pass
self._tags = defaultdict(lambda: '')
self._status = None
for line in info.split("\n"):
if line.startswith("[playing]"):
self._status = "playing"
elif line.startswith("[paused]"):
self._status = "paused"
if line.startswith("["):
timer = line.split()[2]
position = timer.split("/")[0]
dur = timer.split("/")[1]
duration = dur.split(" ")[0]
self._tags.update({'position': position})
self._tags.update({'duration': duration})
if line.startswith("volume"):
value = line.split(" ", 2)[1:]
for option in value:
if option.startswith("repeat: on"):
self._repeat = True
elif option.startswith("repeat: off"):
self._repeat = False
elif option.startswith("random: on"):
self._shuffle = True
elif option.startswith("random: off"):
self._shuffle = False
if line.startswith("tag"):
key, value = line.split(" ", 2)[1:]
self._tags.update({key: value})
if key == "file":
self._tags.update({"file1": os.path.basename(value)})
self._tags.update(
{"file2":
os.path.splitext(os.path.basename(value))[0]})
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,114 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Displays network traffic
* No extra configuration needed
"""
import psutil
import netifaces
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
WIDGET_NAME = 'network_traffic'
class Module(bumblebee.engine.Module):
"""Bumblebee main module """
def __init__(self, engine, config):
super(Module, self).__init__(engine, config)
try:
self._bandwidth = BandwidthInfo()
self._bytes_recv = self._bandwidth.bytes_recv()
self._bytes_sent = self._bandwidth.bytes_sent()
except Exception:
""" We do not want do explode anything """
pass
@classmethod
def state(cls, 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, widgets):
try:
bytes_recv = self._bandwidth.bytes_recv()
bytes_sent = self._bandwidth.bytes_sent()
download_rate = (bytes_recv - self._bytes_recv)
upload_rate = (bytes_sent - self._bytes_sent)
self.update_widgets(widgets, download_rate, upload_rate)
self._bytes_recv, self._bytes_sent = bytes_recv, bytes_sent
except Exception:
""" We do not want do explode anything """
pass
@classmethod
def update_widgets(cls, widgets, download_rate, upload_rate):
"""Update tx/rx widgets with new rates"""
del widgets[:]
widgets.extend((
TrafficWidget(text=download_rate, direction='rx'),
TrafficWidget(text=upload_rate, direction='tx')
))
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)
class TrafficWidget(object):
"""Create a traffic widget with humanized bytes string with proper icon (up/down)"""
def __new__(cls, text, direction):
widget = bumblebee.output.Widget(name='{0}.{1}'.format(WIDGET_NAME, direction))
widget.set('theme.minwidth', '0000000KiB/s')
widget.full_text(cls.humanize(text))
return widget
@staticmethod
def humanize(text):
"""Return humanized bytes"""
humanized_byte_format = bumblebee.util.bytefmt(text)
return '{0}/s'.format(humanized_byte_format)

View file

@ -1,119 +0,0 @@
#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 netifaces
import subprocess
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
super(Module, self).__init__(engine, config, widgets)
self._exclude = tuple(filter(len, self.parameter("exclude", "lo,virbr,docker,vboxnet,veth,br").split(",")))
self._include = self.parameter("include", "").split(",")
self._states = {}
self._states["include"] = []
self._states["exclude"] = []
for state in tuple(filter(len, self.parameter("states", "").split(","))):
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._update_widgets(widgets)
self.iwgetid = bumblebee.util.which("iwgetid")
def update(self, widgets):
self._update_widgets(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):
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 widget in widgets:
widget.set("visited", False)
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 = bumblebee.output.Widget(name=intf)
widgets.append(widget)
# 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)
widget.set("visited", True)
for widget in widgets:
if widget.get("visited") is False:
widgets.remove(widget)
def get_ssid(self, intf):
if self._iswlan(intf):
try:
return subprocess.check_output([self.iwgetid,"-r",intf]).strip().decode('utf-8')
except:
return ""
return ""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,51 +0,0 @@
# 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/)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import os
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._notmuch_count_query = self.parameter("query", "tag:unread AND NOT path:/.*Trash.*/")
self._notmuch_count = self.count_notmuch()
def output(self, widget):
self._notmuch_count = self.count_notmuch()
return str(self._notmuch_count)
def state(self, widgets):
if self._notmuch_count == 0:
return "empty"
return "items"
def count_notmuch(self):
try:
notmuch_count_cmd = "notmuch count " + self._notmuch_count_query
notmuch_count = int(bumblebee.util.execute(notmuch_count_cmd))
return notmuch_count
except Exception:
return -1
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,71 +0,0 @@
# -*- 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
"""
import subprocess
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.utilization))
self._utilization = "Not found: 0 0/0"
def utilization(self, widget):
return self._utilization
def update(self, widgets):
sp = subprocess.Popen(['nvidia-smi', '-q'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out_str = sp.communicate()
out_list = out_str[0].decode("utf-8").split('\n')
title = ""
usedMem = ""
totalMem = ""
temp = ""
name = "not found"
clockMem = ""
clockGpu = ""
fanspeed = ""
for item in out_list:
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,
)

View file

@ -1,76 +0,0 @@
# 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
"""
import os
import threading
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
#list of repositories.
#the last one should always be other
repos = ["core", "extra", "community", "multilib", "testing", "other"]
def get_pacman_info(widget, path):
try:
result = bumblebee.util.execute("{}/../../bin/pacman-updates".format(path))
except:
pass
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])
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.updates)
)
self._count = 0
def updates(self, widget):
if bumblebee.util.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, widgets):
path = os.path.dirname(os.path.abspath(__file__))
if self._count == 0:
thread = threading.Thread(target=get_pacman_info, args=(widgets[0], path))
thread.start()
# TODO: improve this waiting mechanism a bit
self._count += 1
self._count = 0 if self._count > 300 else self._count
def state(self, widget):
weightedCount = sum(map(lambda x: (len(repos)-x[0]) * widget.get(x[1], 0), enumerate(repos)))
if weightedCount < 10:
return "good"
return self.threshold_state(weightedCount, 100, 150)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,68 +0,0 @@
# 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)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import requests
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.pihole_status)
)
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE}
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()
engine.input.register_callback(self, button=bumblebee.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 " + ("up/" + 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:
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, widgets):
self.update_pihole_status()
def state(self, widget):
if self._pihole_status is None:
return []
elif self._pihole_status:
return ["enabled"]
return ["disabled", "warning"]

View file

@ -1,84 +0,0 @@
# pylint: disable=C0111,R0903
"""Periodically checks the RTT of a configurable host using ICMP echos
Requires the following executable:
* ping
Parameters:
* ping.interval: Time in seconds between two RTT checks (defaults to 60)
* 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 bumblebee.input
import bumblebee.output
import bumblebee.engine
def get_rtt(module, widget):
try:
widget.set("rtt-unreachable", False)
res = bumblebee.util.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)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.rtt)
super(Module, self).__init__(engine, config, widget)
widget.set("address", self.parameter("address", "8.8.8.8"))
widget.set("interval", self.parameter("interval", 60))
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)
self._next_check = 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, widgets):
if int(time.time()) < self._next_check:
return
thread = threading.Thread(target=get_rtt, args=(self, widgets[0],))
thread.start()
self._next_check = int(time.time()) + int(widgets[0].get("interval"))
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,119 +0,0 @@
# 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!"'
"""
from __future__ import absolute_import
import datetime
from math import ceil
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = bumblebee.output.Widget(full_text=self.text)
super(Module, self).__init__(engine, config, widgets)
# 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"]
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.timer_play_pause)
engine.input.register_callback(self, button=bumblebee.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, widget):
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.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:
bumblebee.util.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

View file

@ -1,69 +0,0 @@
# 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
"""
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.query)
)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self._chooseNvidia)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE,
cmd=self._chooseIntel)
self.nvidiastring = self.parameter("nvidiastring", "nv")
self.intelstring = self.parameter("intelstring", "it")
def _chooseNvidia(self, event):
bumblebee.util.execute("sudo prime-select nvidia")
def _chooseIntel(self, event):
bumblebee.util.execute("sudo prime-select intel")
def _prev_keymap(self, event):
self._set_keymap(-1)
def query(self, widget):
try:
res = bumblebee.util.execute("prime-select query")
except RuntimeError:
return "n/a"
for line in res.split("\n"):
if not line: continue
if "nvidia" in line:
return self.nvidiastring
if "intel" in line:
return self.intelstring
return "n/a"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,103 +0,0 @@
"""
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
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import re
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.get_progress_text)
)
def get_progress_text(self, widget):
if self.update_progress_info(widget):
width = 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"""
# 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 = bumblebee.util.execute("progress -qW 0.1")
result = extract_wtspeed.match(raw)
if not result:
# Abord speed measures
raw = bumblebee.util.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 state(self, widget):
if self._active():
return "copying"
return "pending"
def _active(self):
"""Checks wether a copy is running or not"""
raw = bumblebee.util.execute("progress -q")
return bool(raw)

View file

@ -1,47 +0,0 @@
"""Displays public IP address
Requires the following python packages:
* requests
Parameters:
* publicip.region: us-central (default), us-east, us-west, uk, de, pl, nl
* publicip.service: web address that returns plaintext ip address (ex. "http://l2.io/ip")
"""
try:
from requests import get
except ImportError:
pass
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.public_ip)
)
self._avail_regions = {"us-east":"http://checkip.amazonaws.com",
"us-central":"http://whatismyip.akamai.com",
"us-west":"http://ipv4bot.whatismyipaddress.com",
"pl":"http://ip.42.pl/raw",
"de":"http://myexternalip.com/raw",
"nl":"http://tnx.nl/ip",
"uk":"http://ident.me"}
self._region = self.parameter("region", "us-central")
self._service = self.parameter("service", "")
self._ip = ""
def public_ip(self, widget):
return self._ip
def update(self, widgets):
try:
if self._service:
self.address = self._service
else:
self.address = self._avail_regions[self._region]
self._ip = get(self.address).text.rstrip()
except Exception:
self._ip = "No Connection"

View file

@ -1,183 +0,0 @@
# 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 bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.volume)
)
try:
if bumblebee.util.asbool(self.parameter("autostart", False)):
bumblebee.util.execute("pulseaudio --start")
except Exception:
pass
self._change = 2
self._change = int(self.parameter("percent_change", "2%").strip("%"))
if self._change < 0 or self._change > 100:
self._change = 2
self._limit = 0
self._limit = int(self.parameter("limit", "0%").strip("%s"))
if self._limit < 0:
self._limit = 0
self._left = 0
self._right = 0
self._mono = 0
self._mute = False
self._failed = False
self._channel = "sink" if self.name == "pasink" else "source"
self._showbars = bumblebee.util.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},
]
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd="pavucontrol")
events = [
{"type": "mute", "action": self.toggle, "button": bumblebee.input.LEFT_MOUSE},
{"type": "volume", "action": self.increase_volume, "button": bumblebee.input.WHEEL_UP},
{"type": "volume", "action": self.decrease_volume, "button": bumblebee.input.WHEEL_DOWN},
]
for event in events:
engine.input.register_callback(self, button=event["button"], cmd=event["action"])
def set_volume(self, amount):
bumblebee.util.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):
bumblebee.util.execute("pactl set-{}-{} @DEFAULT_{}@ {}".format(
self._channel, "mute", self._channel.upper(), "toggle"))
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)
return True
def _default_device(self):
output = bumblebee.util.execute("pactl info")
pattern = "Default {}: ".format("Sink" if self.name == "pasink" else "Source")
for line in output.split("\n"):
if line.startswith(pattern):
return line.replace(pattern, "")
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, bumblebee.output.hbar(float(self._mono)))
return vol
elif self._left == self._right:
vol = "{}%".format(self._left)
if self._showbars:
vol = "{} {}".format(
vol, bumblebee.output.hbar(float(self._left)))
return vol
else:
if self._showbars:
vol = "{} {}{}".format(
vol,
bumblebee.output.hbar(float(self._left)),
bumblebee.output.hbar(float(self._right)))
vol = "{}%/{}%".format(self._left, self._right)
return vol
def update(self, widgets):
try:
self._failed = False
channel = "sinks" if self.name == "pasink" else "sources"
device = self._default_device()
result = bumblebee.util.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:
self._failed = True
if bumblebee.util.asbool(self.parameter("autostart", False)):
try:
bumblebee.util.execute("pulseaudio --start")
self.update(widgets)
except Exception:
pass
def state(self, widget):
if self._mute:
return ["warning", "muted"]
if int(self._left) > int(100):
return ["critical", "unmuted"]
return ["unmuted"]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,129 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays the current color temperature of redshift
Requires the following executable:
* redshift
Parameters:
* redshift.location : location provider, either of "geoclue2" (default), \
"ipinfo" (requires the requests package), or "manual"
* redshift.lat : latitude if location is set to "manual"
* redshift.lon : longitude if location is set to "manual"
"""
import threading
try:
import requests
except ImportError:
pass
import bumblebee.input
import bumblebee.output
import bumblebee.engine
def is_terminated():
for thread in threading.enumerate():
if thread.name == "MainThread" and not thread.is_alive():
return True
return False
def get_redshift_value(widget, location, lat, lon):
while True:
if is_terminated():
return
widget.get("condition").acquire()
while True:
try:
widget.get("condition").wait(1)
except RuntimeError:
continue
break
widget.get("condition").release()
command = ["redshift", "-p", "-l"]
if location == "manual":
command.append(lat + ":" + lon)
else:
command.append("geoclue2")
try:
res = bumblebee.util.execute(" ".join(command))
except Exception:
res = ""
widget.set("temp", "n/a")
widget.set("transition", None)
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")
widget.set("transition", " ".join(line.split(" ")[2:]))
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.text)
super(Module, self).__init__(engine, config, widget)
self._location = self.parameter("location", "geoclue2")
self._lat = self.parameter("lat", None)
self._lon = self.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 self._location == "manual" and (self._lat is None
or self._lon is None):
self._location == "geoclue2"
if self._location == "ipinfo":
try:
location_url = "http://ipinfo.io/json"
location = requests.get(location_url).json()
self._lat, self._lon = location["loc"].split(",")
self._lat = str(float(self._lat))
self._lon = str(float(self._lon))
self._location = "manual"
except Exception:
# Fall back to geoclue2.
self._location = "geoclue2"
self._text = ""
self._condition = threading.Condition()
widget.set("condition", self._condition)
self._thread = threading.Thread(target=get_redshift_value,
args=(widget, self._location,
self._lat, self._lon))
self._thread.start()
self._condition.acquire()
self._condition.notify()
self._condition.release()
def text(self, widget):
return "{}".format(self._text)
def update(self, widgets):
widget = widgets[0]
self._condition.acquire()
self._condition.notify()
self._condition.release()
temp = widget.get("temp", "n/a")
self._text = temp
transition = widget.get("transition", None)
if transition:
self._text = "{} {}".format(temp, transition)
def state(self, widget):
return widget.get("state", None)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,61 +0,0 @@
# 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 bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
possible_orientations = ["normal", "left", "inverted", "right"]
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
self._engine = engine
super(Module, self).__init__(engine, config, widgets)
self.update_widgets(widgets)
def update_widgets(self, widgets):
for line in bumblebee.util.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 = bumblebee.output.Widget(full_text=display, name=display)
self._engine.input.register_callback(widget, button=bumblebee.input.LEFT_MOUSE, cmd=self._toggle)
widget.set("orientation", orientation)
widgets.append(widget)
def update(self, widgets):
if len(widgets) <= 0:
self.update_widgets(widgets)
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)
bumblebee.util.execute("xrandr --output {} --rotation {}".format(widget.name, new_orientation))
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,312 +0,0 @@
# 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
"""
try:
import feedparser
DEPENDENCIES_OK = True
except ImportError:
DEPENDENCIES_OK = False
import webbrowser
import time
import os
import tempfile
import logging
import random
import re
import json
import bumblebee.input
import bumblebee.output
import bumblebee.engine
# pylint: disable=too-many-instance-attributes
class Module(bumblebee.engine.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, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.ticker_update if DEPENDENCIES_OK else self._show_error)
)
# Use BBC newsfeed as demo:
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
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._open)
engine.input.register_callback(self, button=bumblebee.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 _show_error(self, _):
return "Please install feedparser first"
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 update(self, widgets):
pass
def state(self, _):
return self._state
def _create_news_element(self, item, overlay_title):
try:
timestr = "" if item['published'] == 0 else str(time.ctime(item['published']))
except Exception as exc:
logging.error(str(exc))
raise e
element = "<div class='item' onclick=window.open('"+item['link']+"')>"
element += "<div class='titlecontainer'>"
element += " <img "+("" if item['image'] else "class='noimg' ")+"src='"+item['image']+"'>"
element += " <div class='title"+(" overlay" if overlay_title else "")+"'>"+("<span class='star'>&#x2605;</span>" if item['new'] else "")+item['title']+"</div>"
element += "</div>"
element += "<div class='summary'>"+item['summary']+"</div>"
element += "<div class='info'><span class='author'>"+item['feed']+"</span><span class='published'>"+timestr+"</span></div>"
element += "</div>"
return element
def _create_news_section(self, newspaper_items):
style = random.randint(0, 3)
section = "<table><tr class='style"+str(style)+"'>"
for i in range(0, 3):
section += "<td><div class='itemcontainer'>"
for _ in range(0, self.LAYOUT_STYLES_ITEMS[style][i]):
if newspaper_items:
section += self._create_news_element(newspaper_items[0], self.LAYOUT_STYLES_ITEMS[style][i] != 3)
del newspaper_items[0]
section += "</div></td>"
section += "</tr></table>"
return section
def _create_newspaper(self, _):
content = ""
newspaper_items = self._items[:]
self._check_history(newspaper_items, 'newspaper')
# Make sure new items are always listed first, independent of publish date
newspaper_items.sort(key=lambda i: i['published']+(10000000 if i['new'] else 0), reverse=True)
while newspaper_items:
content += self._create_news_section(newspaper_items)
open(self._newspaper_filename, "w").write(HTML_TEMPLATE.replace("[[CONTENT]]", content))
webbrowser.open("file://"+self._newspaper_filename)
self._update_history('newspaper')
self._save_history()
HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<script>
window.onload = function() {
var images = document.getElementsByTagName('img');
// Remove very small images
for(var i = 0; i < images.length; i++) {
if (images[i].naturalWidth<50 || images[i].naturalHeight<50) {
images[i].src = ''
images[i].className+=' noimg'
}
}
}
</script>
</head>
<style>
body {background: #eee; font-family: Helvetica neue;}
td {background: #fff; height: 100%;}
tr.style0 td {width: 33%;}
tr.style1 td {width: 20%;}
tr.style1 td:last-child {width: 60%;}
tr.style2 td {width: 20%;}
tr.style2 td:first-child {width: 60%;}
tr.style3 td {width: 20%;}
tr.style3 td:nth-child(2) {width: 60%;}
img {width: 100%; display: block; }
img.noimg {min-height:250px; background: #1299c8;}
#content {width: 1500px; margin: auto; background: #eee; padding: 1px;}
#newspapertitle {text-align: center; font-size: 60px; font-family: Arial Black; background: #1299c8; font-style: Italic; padding: 10px; color: #fff; }
.star {color: #ffa515; font-size: 24px;}
.section {display: flex;}
.column {display: flex;}
.itemcontainer {width: 100%; height: 100%; position: relative; display: inline-table;}
.item {cursor: pointer; }
.titlecontainer {position: relative;}
.title.overlay {font-family: Arial; position: absolute; bottom: 10px; color: #fff; font-weight: bold; text-align: right; max-width: 75%; right: 10px; font-size: 23px; text-shadow: 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000, -1px 0 0 #000;}
.title:not(.overlay) {font-weight: bold; padding: 0px 10px;}
.summary {color: #444; padding: 10px 10px 0px 10px; font-family: Times new roman; font-size: 18px; flex: 1;max-height: 105px; overflow: hidden;}
.info {color: #aaa; font-family: arial; font-size: 13px; padding: 10px;}
.published {float: right;}
</style>
<body>
<div id='content'>
<div id='newspapertitle'>Bumblebee Daily</div>
[[CONTENT]]
</div>
</body>
</html>"""
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,128 +0,0 @@
# -*- 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)
"""
import re
import json
import logging
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
log = logging.getLogger(__name__)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=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 = bumblebee.util.asbool(self.parameter("json", "false"))
self._freq = bumblebee.util.asbool(self.parameter("show_freq", "true"))
engine.input.register_callback(self, button=bumblebee.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 = bumblebee.util.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(bumblebee.util.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 = bumblebee.util.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 u"{}°c @ {}".format(self._temperature, self._mhz)
else:
return u"{}°c".format(self._temperature)
def update(self, widgets):
self._temperature = self.get_temp()
if self._freq:
self._mhz = self.get_mhz()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,156 +0,0 @@
# -*- 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)
"""
import re
import bumblebee.util
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config, None)
self._chip = self.parameter("chip", "")
self._data = {}
self._update()
self.widgets(self._create_widgets())
def update(self, widgets):
self._update()
for widget in 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):
widgets = []
show_temp = bumblebee.util.asbool(self.parameter("showtemp", "true"))
show_fan = bumblebee.util.asbool(self.parameter("showfan", "true"))
show_other = bumblebee.util.asbool(self.parameter("showother", "false"))
if bumblebee.util.asbool(self.parameter("showcpu", "true")):
widget = bumblebee.output.Widget(full_text=self._cpu)
widget.set("type", "cpu")
widgets.append(widget)
for adapter in self._data:
for package in self._data[adapter]:
if bumblebee.util.asbool(self.parameter("showname", "false")):
widget = bumblebee.output.Widget(full_text=package)
widget.set("data", self._data[adapter][package])
widget.set("package", package)
widget.set("field", "")
widget.set("adapter", adapter)
widgets.append(widget)
for field in self._data[adapter][package]:
widget = bumblebee.output.Widget()
widget.set("package", package)
widget.set("field", field)
widget.set("adapter", adapter)
if "temp" in field and show_temp:
# seems to be a temperature
widget.set("type", "temp")
widgets.append(widget)
if "fan" in field and show_fan:
# seems to be a fan
widget.set("type", "fan")
widgets.append(widget)
elif show_other:
# everything else
widget.set("type", "other")
widgets.append(widget)
return widgets
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(u"{:0.01f}°C".format(data["input"]))
elif "fan" in widget.get("field"):
widget.full_text(u"{:0.0f}RPM".format(data["input"]))
else:
widget.full_text(u"{:0.0f}".format(data["input"]))
def _update(self):
output = bumblebee.util.execute("sensors -u {}".format(self._chip))
self._data = self._parse(output)
def _parse(self, data):
output = {}
package = ""
adapter = None
chip = None
for line in data.split("\n"):
if "Adapter" in line:
# new adapter
line = line.replace("Adapter: ", "")
output[chip + " " + line] = {}
adapter = chip + " " + line
chip = line #default - line before adapter is always the chip
if not adapter: continue
key, value = (line.split(":") + ["", ""])[:2]
if not line.startswith(" "):
# assume this starts a new package
if package in output[adapter] and output[adapter][package] == {}:
del output[adapter][package]
output[adapter][key] = {}
package = key
else:
# feature for this chip
try:
name, variant = (key.strip().split("_", 1) + ["",""])[:2]
if not name in output[adapter][package]:
output[adapter][package][name] = { }
if variant:
output[adapter][package][name][variant] = {}
output[adapter][package][name][variant] = float(value)
except Exception as e:
pass
return output
def _cpu(self, _):
mhz = None
try:
output = open("/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq").read()
mhz = int(float(output)/1000.0)
except:
output = open("/proc/cpuinfo").read()
m = re.search(r"cpu MHz\s+:\s+(\d+)", output)
if m:
mhz = int(m.group(1))
else:
m = re.search(r"BogoMIPS\s+:\s+(\d+)", output)
if m:
return "{} BogoMIPS".format(int(m.group(1)))
if not mhz:
return "n/a"
if mhz < 1000:
return "{} MHz".format(mhz)
else:
return "{:0.01f} GHz".format(float(mhz)/1000.0)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,93 +0,0 @@
# pylint: disable=C0111,R0903,W1401
""" Execute command in shell and print result
Few command examples:
'ping 1.1.1.1 -c 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)
"""
import os
import subprocess
import threading
import bumblebee.engine
import bumblebee.input
import bumblebee.output
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widget = bumblebee.output.Widget(full_text=self.get_output)
super(Module, self).__init__(engine, config, widget)
if self.parameter('interval'):
self.interval(self.parameter('interval'))
self._command = self.parameter('command')
self._async = bumblebee.util.asbool(self.parameter('async'))
if self._async:
self._output = 'Computing...'
self._current_thread = None
else:
self._output = ''
# LMB and RMB will update output regardless of timer
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self.update)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self.update)
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 = self._get_command_output_or_error(self._command)
return
# if previous thread didn't end yet then don't do anything
if self._current_thread:
return
# spawn new thread to execute command and pass callback method to get output from it
self._current_thread = threading.Thread(target=self._run_command_in_thread,
args=(self._command, self._output_function))
self._current_thread.start()
@staticmethod
def _get_command_output_or_error(command):
try:
command_output = subprocess.check_output(command,
executable=os.environ.get('SHELL'),
shell=True,
stderr=subprocess.STDOUT)
return command_output.decode('utf-8').strip()
except subprocess.CalledProcessError as exception:
exception_output = exception.output.decode('utf-8').replace('\n', '')
return 'Status:{} output:{}'.format(exception.returncode, exception_output)
def _run_command_in_thread(self, command, output_callback):
output_callback(self._get_command_output_or_error(command))
def _output_function(self, text):
self._output = text
# clear this thread data, so next update will spawn a new one
self._current_thread = None
# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,70 +0,0 @@
# 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)
"""
import logging
import bumblebee.engine
import bumblebee.output
import bumblebee.input
LINK = "https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
LABEL = "Click me"
class Module(bumblebee.engine.Module):
""" Shortcut module."""
def __init__(self, engine, config):
widgets = []
self._engine = engine
super(Module, self).__init__(engine, config, widgets)
self._labels = self.parameter("labels", "{}".format(LABEL))
self._cmds = self.parameter("cmds", "firefox {}".format(LINK))
self._delim = self.parameter("delim", ";")
self.update_widgets(widgets)
def update_widgets(self, widgets):
""" 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 = bumblebee.output.Widget(full_text=label)
self._engine.input.register_callback(widget, button=bumblebee.input.LEFT_MOUSE, cmd=cmd)
widgets.append(widget)
def update(self, widgets):
if len(widgets) <= 0:
self.update_widgets(widgets)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,150 +0,0 @@
#!/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%%"
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import requests
import threading
import re
import json
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(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(
engine, config, bumblebee.output.Widget(full_text=self.getState)
)
engine.input.register_callback(
self, button=bumblebee.input.LEFT_MOUSE, cmd=self.__forceReload
)
self._data = {}
self._error = None
self._threadingCount = 0
# The URL representing the api endpoint
self._url = self.parameter("url", default="http://club.entropia.de/spaceapi")
self._format = self.parameter(
"format", default=u"%%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, widgets):
if self._threadingCount == 0:
thread = threading.Thread(target=self.get_api_async, args=())
thread.start()
self._threadingCount = (
0 if self._threadingCount > 300 else self._threadingCount + 1
)
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"
# left_mouse_button handler
def __forceReload(self, event):
self._threadingCount += 300
self._error = "RELOADING"
# Flattens the JSON structure recursively, e.g. ["space"]["open"]
# becomes ["space.open"]
def __flatten(self, json):
out = {}
for key in json:
value = json[key]
if type(value) is dict:
flattened_key = self.__flatten(value)
for fk in flattened_key:
out[key + "." + fk] = flattened_key[fk]
else:
out[key] = value
return out
# Author: Tobias Manske <tobias@chaoswg.xyz>
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,23 +0,0 @@
# pylint: disable=C0111,R0903
"""Draws a widget with configurable text content.
Parameters:
* spacer.text: Widget contents (defaults to empty string)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._text = self.parameter("text", "")
def text(self, widget):
return self._text
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,87 +0,0 @@
# 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
"""
import sys
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
try:
import dbus
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.spotify)
)
buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE,
"RIGHT_CLICK":bumblebee.input.RIGHT_MOUSE,
"MIDDLE_CLICK":bumblebee.input.MIDDLE_MOUSE,
"SCROLL_UP":bumblebee.input.WHEEL_UP,
"SCROLL_DOWN":bumblebee.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."
engine.input.register_callback(self, button=buttons[prev_button],
cmd=cmd + "Previous")
engine.input.register_callback(self, button=buttons[next_button],
cmd=cmd + "Next")
engine.input.register_callback(self, button=buttons[pause_button],
cmd=cmd + "PlayPause")
@scrollable
def spotify(self, widget):
return self.string_song
def hidden(self):
return self.string_song == ""
def update(self, widgets):
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=u"\u25B6" if playback_status=="Playing" else u"\u258D\u258D" if playback_status=="Paused" else "",)
except Exception:
self._song = ""
@property
def string_song(self):
if sys.version_info.major < 3:
return unicode(self._song)
return str(self._song)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,61 +0,0 @@
# -*- 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)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.util
import json
import requests
import logging
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.value)
)
self._symbols = self.parameter('symbols', '')
self._change = bumblebee.util.asbool(self.parameter('change', True))
self._value = None
self.interval_factor(60)
self.interval(60)
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 = 'n/a' if not 'symbol' in symbol else symbol['symbol']
currency = 'USD' if not 'currency' in symbol else symbol['currency']
val = 'n/a' if not valkey in symbol else '{:.2f}'.format(symbol[valkey])
results.append('{} {} {}'.format(sym, val, currency))
return u' '.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 requests.get(url).text.strip()
else:
logging.error('unable to retrieve stock exchange rate')
return None
def update(self, widgets):
self._value = self.fetch()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,111 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays sunrise and sunset times
Parameters:
* cpu.lat : Latitude of your location
* cpu.lon : Longitude of your location
"""
try:
from suntime import Sun, SunTimeException
except ImportError:
pass
try:
import requests
except ImportError:
pass
try:
from dateutil.tz import tzlocal
except ImportError:
pass
import datetime
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(
engine, config,
bumblebee.output.Widget(full_text=self.suntimes)
)
self.interval(3600)
self._lat = self.parameter("lat", None)
self._lon = self.parameter("lon", None)
try:
if not self._lat or not self._lon:
location_url = "http://ipinfo.io/json"
location = requests.get(location_url).json()
self._lat, self._lon = location["loc"].split(",")
self._lat = float(self._lat)
self._lon = float(self._lon)
except Exception:
pass
self.update(None)
def suntimes(self, _):
if self._sunset and self._sunrise:
if self._isup:
return u"\u21A7{} \u21A5{}".format(
self._sunset.strftime('%H:%M'),
self._sunrise.strftime('%H:%M'))
return u"\u21A5{} \u21A7{}".format(self._sunrise.strftime('%H:%M'),
self._sunset.strftime('%H:%M'))
return "?"
def _calculate_times(self):
self._isup = False
try:
sun = Sun(self._lat, self._lon)
except Exception:
self._sunrise = None
self._sunset = None
return
order_matters = True
try:
self._sunrise = sun.get_local_sunrise_time()
except SunTimeException:
self._sunrise = "no sunrise"
order_matters = False
try:
self._sunset = 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 = sun.get_local_sunrise_time(tomorrow)
self._sunset = 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 = sun.get_local_sunrise_time(tomorrow)
except SunTimeException:
self._sunrise = "no sunrise"
return
self._isup = True
def update(self, widgets):
if not self._lat or not self._lon:
self._sunrise = None
self._sunset = None
self._calculate_times()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,98 +0,0 @@
# -*- 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')
"""
import logging
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import bumblebee.popup_v2
import functools
try:
import Tkinter as tk
import tkMessageBox as tkmessagebox
except ImportError:
# python 3
try:
import tkinter as tk
from tkinter import messagebox as tkmessagebox
except ImportError:
logging.warning("failed to import tkinter - bumblebee popups won't work!")
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._confirm = True
if self.parameter("confirm", "true") == "false":
self._confirm = False
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup)
def update(self, widgets):
pass
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:
bumblebee.util.execute(command)
def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu()
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(bumblebee.util.execute, switch_user_cmd))
menu.add_menuitem("lock", callback=functools.partial(bumblebee.util.execute, lock_cmd))
menu.add_menuitem("suspend", callback=functools.partial(bumblebee.util.execute, suspend_cmd))
menu.add_menuitem("hibernate", callback=functools.partial(bumblebee.util.execute, hibernate_cmd))
menu.show(widget)
def state(self, widget):
return []

View file

@ -1,42 +0,0 @@
"""Displays the number of pending tasks in TaskWarrior.
Requires the following library:
* taskw
Parameters:
* taskwarrior.taskrc : path to the taskrc file (defaults to ~/.taskrc)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
try:
from taskw import TaskWarrior
except:
pass
class Module(bumblebee.engine.Module):
"""TaskWarrior module."""
def __init__(self, engine, config):
"""Initialize taskwarrior module."""
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(
full_text=self.output))
self._pending_tasks_count = "0"
def update(self, widgets):
"""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_count = str(len(pending_tasks))
except:
self._pending_tasks_count = 'Error'
def output(self, _):
"""Format the task counter to output in bumblebee."""
return "{}".format(self._pending_tasks_count)

View file

@ -1,14 +0,0 @@
# pylint: disable=C0111,R0903
"""Test module
"""
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="test")
)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,85 +0,0 @@
# 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
"""
import threading
try:
import i3ipc
except ImportError:
pass
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from bumblebee.output import scrollable
_no_title = "n/a"
class Module(bumblebee.engine.Module):
"""Window title module."""
def __init__(self, engine, config):
super(Module, self).__init__(
engine,
config
)
# parsing of parameters
self._scroll = bumblebee.util.asbool(self.parameter("scroll", False))
self._max = int(self.parameter("max", 64))
self._placeholder = self.parameter("placeholder", "...")
# set output of the module
self.widgets(bumblebee.output.Widget(full_text=
self._scrolling_focused_title if self._scroll else self._focused_title))
# create a connection with i3ipc
try:
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()
except:
pass
# initialize the first title
self._pollTitle()
def _focused_title(self, widget):
return self._title
@scrollable
def _scrolling_focused_title(self, widget):
return self._full_title
def _pollTitle(self):
"""Updating current title."""
try:
self._full_title = self._i3.get_tree().find_focused().name
except:
self._full_title = _no_title
if self._full_title is None:
self._full_title = _no_title
if not self._scroll:
# cut the text if it is too long
if len(self._full_title) > self._max:
self._title = self._full_title[0:self._max - len(self._placeholder)]
self._title = "{}{}".format(self._title, self._placeholder)
else:
self._title = self._full_title
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,45 +0,0 @@
# 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)
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import os.path
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._doc = os.path.expanduser(self.parameter("file", "~/Documents/todo.txt"))
self._todos = self.count_items()
def output(self, widget):
self._todos = self.count_items()
return str(self._todos)
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

View file

@ -1,110 +0,0 @@
# 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)
"""
import time
import psutil
import netifaces
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
super(Module, self).__init__(engine, config, widgets)
self._exclude = tuple(filter(len, self.parameter("exclude", "lo,virbr,docker,vboxnet,veth").split(",")))
self._status = ""
self._showname = bumblebee.util.asbool(self.parameter("showname", True))
self._prev = {}
self._states = {}
self._lastcheck = 0
self._states["include"] = []
self._states["exclude"] = []
for state in tuple(filter(len, self.parameter("states", "").split(","))):
if state[0] == "^":
self._states["exclude"].append(state[1:])
else:
self._states["include"].append(state)
self._update_widgets(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, widgets):
self._update_widgets(widgets)
def create_widget(self, widgets, name, txt=None, attributes={}):
widget = bumblebee.output.Widget(name=name)
widget.full_text(txt)
widgets.append(widget)
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 _update_widgets(self, widgets):
interfaces = [i for i in netifaces.interfaces() if not i.startswith(self._exclude)]
del 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 not interface: interface = "lo"
state = "down"
if len(self.get_addresses(interface)) > 0:
state = "up"
elif bumblebee.util.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(widgets, name, interface)
for direction in ["rx", "tx"]:
name = "traffic.{}-{}".format(direction, interface)
widget = self.create_widget(widgets, name, attributes={"theme.minwidth": "1000.00MB"})
prev = self._prev.get(name, 0)
speed = bumblebee.util.bytefmt((int(data[direction]) - int(prev))/timediff)
txtspeed ='{0}/s'.format(speed)
widget.full_text(txtspeed)
self._prev[name] = data[direction]
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,39 +0,0 @@
#pylint: disable=C0111,R0903
"""Toggle twmn notifications."""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="")
)
self._paused = False
# Make sure that twmn is currently not paused
try:
bumblebee.util.execute("killall -SIGUSR2 twmnd")
except:
pass
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.toggle_status
)
def toggle_status(self, event):
self._paused = not self._paused
try:
if self._paused:
bumblebee.util.execute("systemctl --user start twmnd")
else:
bumblebee.util.execute("systemctl --user stop twmnd")
except:
self._paused = not self._paused # toggling failed
def state(self, widget):
if self._paused:
return ["muted"]
return ["unmuted"]

View file

@ -1,30 +0,0 @@
# pylint: disable=C0111,R0903
"""Displays the system uptime."""
# Use absolute_import because there's already a datatime module
# in the same directory
from __future__ import absolute_import
import bumblebee.input
import bumblebee.output
import bumblebee.engine
from datetime import timedelta
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._uptime = ""
def output(self, _):
return "{}".format(self._uptime)
def update(self, widgets):
with open('/proc/uptime', 'r') as f:
uptime_seconds = int(float(f.readline().split()[0]))
self._uptime = timedelta(seconds = uptime_seconds)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,81 +0,0 @@
# pylint: disable=C0111,R0903
"""Copy passwords from a password store into the clipboard (currently supports only "pass")
Many thanks to [@bbernhard](https://github.com/bbernhard) for the idea!
Parameters:
* vault.duration: Duration until password is cleared from clipboard (defaults to 30)
* vault.location: Location of the password store (defaults to ~/.password-store)
* vault.offx: x-axis offset of popup menu (defaults to 0)
* vault.offy: y-axis offset of popup menu (defaults to 0)
"""
# TODO:
# - support multiple backends by abstracting the menu structure into a tree
# - build the menu and the actions based on that abstracted tree
#
import os
import time
import threading
import bumblebee.util
import bumblebee.popup_v2
import bumblebee.input
import bumblebee.output
import bumblebee.engine
def build_menu(parent, current_directory, callback):
with os.scandir(current_directory) as it:
for entry in it:
if entry.name.startswith("."): continue
if entry.is_file():
name = entry.name[:entry.name.rfind(".")]
parent.add_menuitem(name, callback=lambda : callback(os.path.join(current_directory, name)))
else:
submenu = bumblebee.popup_v2.PopupMenu(parent, leave=False)
build_menu(submenu, os.path.join(current_directory, entry.name), callback)
parent.add_cascade(entry.name, submenu)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.text)
)
self._duration = int(self.parameter("duration", 30))
self._offx = int(self.parameter("offx", 0))
self._offy = int(self.parameter("offy", 0))
self._path = os.path.expanduser(self.parameter("location", "~/.password-store/"))
self._reset()
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup)
def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu(leave=False)
build_menu(menu, self._path, self._callback)
menu.show(widget, offset_x=self._offx, offset_y=self._offy)
def _reset(self):
self._timer = None
self._text = str(self.parameter("text", "<click-for-password>"))
def _callback(self, secret_name):
secret_name = secret_name.replace(self._path, "") # remove common path
if self._timer:
self._timer.cancel()
# bumblebee.util.execute hangs for some reason
os.system("PASSWORD_STORE_CLIP_TIME={} pass -c {} > /dev/null 2>&1".format(self._duration, secret_name))
self._timer = threading.Timer(self._duration, self._reset)
self._timer.start()
self._start = int(time.time())
self._text = secret_name
def text(self, widget):
if self._timer:
return "{} ({}s)".format(self._text, self._duration - (int(time.time()) - self._start))
return self._text
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,97 +0,0 @@
# 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:
* 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>
"""
import logging
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import functools
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.vpn_status)
)
self._connected_vpn_profile = None
self._selected_vpn_profile = None
res = bumblebee.util.execute("nmcli -g NAME,TYPE c")
lines = res.splitlines()
self._vpn_profiles = []
for line in lines:
info = line.split(':')
try:
if info[1] == "vpn":
self._vpn_profiles.append(info[0])
except:
pass
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd=self.popup)
def update(self, widgets):
try:
res = bumblebee.util.execute("nmcli -g NAME,TYPE,DEVICE con")
lines = res.splitlines()
self._connected_vpn_profile = None
for line in lines:
info = line.split(':')
if info[1] == "vpn" and info[2] != "":
self._connected_vpn_profile = info[0]
except Exception as e:
logging.exception("Couldn't 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_vpn_disconnect(self):
try:
bumblebee.util.execute("nmcli c down \"{vpn}\""
.format(vpn=self._connected_vpn_profile))
self._connected_vpn_profile = None
except Exception as e:
logging.exception("Couldn't disconnect VPN connection")
def _on_vpn_connect(self, name):
self._selected_vpn_profile = name
try:
bumblebee.util.execute("nmcli c up \"{vpn}\""
.format(vpn=self._connected_vpn_profile))
self._connected_vpn_profile = name
except Exception as e:
logging.exception("Couldn't establish VPN connection")
self._connected_vpn_profile = None
def popup(self, widget):
menu = bumblebee.popup_v2.PopupMenu()
if self._connected_vpn_profile is not None:
menu.add_menuitem("Disconnect", callback=self._on_vpn_disconnect)
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_vpn_connect, vpn_profile))
menu.show(widget)
def state(self, widget):
return []

View file

@ -1,138 +0,0 @@
# -*- 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 from http://ipinfo.io
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
"""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
import re
try:
import requests
from requests.exceptions import RequestException
except ImportError:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.output)
)
self._temperature = 0
self._apikey = self.parameter("apikey", "af7bfe22287c652d032a3064ffa44088")
self._location = self.parameter("location", "auto")
if "," in self._location:
self._location = self._location.split(",")
else:
self._location = [self._location]
self._index = 0
self._showcity = bumblebee.util.asbool(self.parameter("showcity", True))
self._showminmax = bumblebee.util.asbool(self.parameter("showminmax", False))
self._unit = self.parameter("unit", "metric")
self._valid = False
self.interval_factor(60)
self.interval(15)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._next_location)
engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd=self._prev_location)
def _next_location(self, event):
self._index = 0 if self._index >= len(self._location) - 1 else self._index + 1
self.update(self.widgets())
def _prev_location(self, event):
self._index = len(self._location)-1 if self._index <= 0 else self._index - 1
self.update(self.widgets())
def _unit_suffix(self):
if self._unit == "metric":
return "C"
if self._unit == "kelvin":
return "K"
if self._unit == "imperial":
return "F"
return ""
def temperature(self):
return u"{}°{}".format(self._temperature, self._unit_suffix())
def tempmin(self):
return u"{}°{}".format(self._tempmin, self._unit_suffix())
def tempmax(self):
return u"{}°{}".format(self._tempmax, self._unit_suffix())
def city(self):
city = re.sub('[_-]', ' ', self._city)
return u"{} ".format(city)
def output(self, widget):
if not self._valid:
return u"?"
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']
else:
return []
return []
def update(self, widgets):
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":
location_url = "http://ipinfo.io/json"
location = requests.get(location_url).json()
coord = location["loc"].split(",")
weather_url = "{url}&lat={lat}&lon={lon}".format(url=weather_url, lat=coord[0], lon=coord[1])
elif self._location[self._index].isdigit():
weather_url = "{url}&id={id}".format(url=weather_url, id=self._location[self._index])
else:
weather_url = "{url}&q={city}".format(url=weather_url, city=self._location[self._index])
weather = requests.get(weather_url).json()
self._city = weather['name']
self._temperature = int(weather['main']['temp'])
self._tempmin = int(weather['main']['temp_min'])
self._tempmax = int(weather['main']['temp_max'])
self._weather = weather['weather'][0]['main'].lower()
self._valid = True
except Exception:
self._valid = False
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,17 +0,0 @@
#pylint: disable=C0111,R0903
"""Opens a random xkcd comic in the browser."""
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text="xkcd")
)
engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE,
cmd="xdg-open https://c.xkcd.com/random/comic/"
)

View file

@ -1,118 +0,0 @@
# pylint: disable=C0111,R0903
"""Shows a widget for each connected screen and allows the user to enable/disable screens.
Parameters:
* xrandr.overwrite_i3config: If set to 'true', this module assembles a new i3 config
every time a screen is enabled or disabled by taking the file "~/.config/i3/config.template"
and appending a file "~/.config/i3/config.<screen name>" for every screen.
* xrandr.autoupdate: If set to 'false', does *not* invoke xrandr automatically. Instead, the
module will only refresh when displays are enabled or disabled (defaults to true)
Requires the following python module:
* (optional) i3 - if present, the need for updating the widget list is auto-detected
Requires the following executable:
* xrandr
"""
import os
import re
import sys
import bumblebee.util
import bumblebee.input
import bumblebee.output
import bumblebee.engine
try:
import i3
except:
pass
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
self._engine = engine
super(Module, self).__init__(engine, config, widgets)
self._autoupdate = bumblebee.util.asbool(self.parameter("autoupdate", True))
self._needs_update = True
try:
i3.Subscription(self._output_update, "output")
except:
pass
def _output_update(self, event, data, _):
self._needs_update = True
def update_widgets(self, widgets):
new_widgets = []
if self._autoupdate == False and self._needs_update == False:
return
self._needs_update = False
for line in bumblebee.util.execute("xrandr -q").split("\n"):
if not " connected" in line:
continue
display = line.split(" ", 2)[0]
m = re.search(r'\d+x\d+\+(\d+)\+\d+', line)
widget = self.widget(display)
if not widget:
widget = bumblebee.output.Widget(full_text=display, name=display)
self._engine.input.register_callback(widget, button=1, cmd=self._toggle)
self._engine.input.register_callback(widget, button=3, cmd=self._toggle)
new_widgets.append(widget)
widget.set("state", "on" if m else "off")
widget.set("pos", int(m.group(1)) if m else sys.maxsize)
while len(widgets) > 0:
del widgets[0]
for widget in new_widgets:
widgets.append(widget)
if self._autoupdate == False:
widget = bumblebee.output.Widget(full_text="")
widget.set("state", "refresh")
self._engine.input.register_callback(widget, button=1, cmd=self._refresh)
widgets.append(widget)
def update(self, widgets):
self.update_widgets(widgets)
def state(self, widget):
return widget.get("state", "off")
def _refresh(self, event):
self._needs_update = True
def _toggle(self, event):
self._needs_update = True
path = os.path.dirname(os.path.abspath(__file__))
if bumblebee.util.asbool(self.parameter("overwrite_i3config", False)) == True:
toggle_cmd = "{}/../../bin/toggle-display.sh".format(path)
else:
toggle_cmd = "xrandr"
widget = self.widget_by_id(event["instance"])
if widget.get("state") == "on":
bumblebee.util.execute("{} --output {} --off".format(toggle_cmd, widget.name))
else:
first_neighbor = next((widget for widget in self.widgets() if widget.get("state") == "on"), None)
last_neighbor = next((widget for widget in reversed(self.widgets()) if widget.get("state") == "on"), None)
neighbor = first_neighbor if event["button"] == bumblebee.input.LEFT_MOUSE else last_neighbor
if neighbor is None:
bumblebee.util.execute("{} --output {} --auto".format(toggle_cmd, widget.name))
else:
bumblebee.util.execute("{} --output {} --auto --{}-of {}".format(toggle_cmd, widget.name,
"left" if event.get("button") == bumblebee.input.LEFT_MOUSE else "right",
neighbor.name))
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,33 +0,0 @@
# 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.
"""
import yubico
import bumblebee.input
import bumblebee.output
import bumblebee.engine
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
super(Module, self).__init__(engine, config,
bumblebee.output.Widget(full_text=self.keystate))
self._keystate = "No YubiKey"
def keystate(self, widget):
return self._keystate
def update(self, widget):
try:
self._keystate = "YubiKey: " + str(yubico.find_yubikey(debug=False).serial())
except yubico.yubico_exception.YubicoError:
self._keystate = "No YubiKey"
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,187 +0,0 @@
"""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!
"""
import time
import logging
from pkg_resources import parse_version
import bumblebee.engine
from bumblebee.util import execute, bytefmt, asbool
log = logging.getLogger(__name__)
class Module(bumblebee.engine.Module):
def __init__(self, engine, config):
widgets = []
super(Module, self).__init__(engine, config, widgets)
self._includelist = set(filter(lambda x: len(x) > 0,
self.parameter("list", default="").split(',')))
self._format = self.parameter("format", default="{name} {shortstatus} {used}/{size} " +
"({percentfree}%)")
self._usesudo = asbool(self.parameter("sudo", default=False))
self._showio = asbool(self.parameter("showio", default=True))
self._ioformat = self.parameter("ioformat", default="{band}")
self._warnfree = int(self.parameter("warnfree", default=10))
self._update_widgets(widgets)
def update(self, widgets):
self._update_widgets(widgets)
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
def _update_widgets(self, 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 = execute(('sudo ' if self._usesudo else '') + 'zpool list -H').split('\n')
for widget in widgets:
widget.set("visited", False)
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 = bumblebee.output.Widget(name=name)
widget.set("last_iostat", [0, 0, 0, 0])
widget.set("last_timestamp", 0)
widgets.append(widget)
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 = bumblebee.output.Widget(name=rname)
widget_w = bumblebee.output.Widget(name=wname)
widgets.extend([widget_r, widget_w])
for w in [widget_r, widget_w]:
w.set("theme.minwidth", self._ioformat.format(ops=9999,
band=bytefmt(999.99*(1024**2))))
w.set("visited", True)
widget_w.full_text(self._ioformat.format(ops=round(writes),
band=bytefmt(nwritten)))
widget_r.full_text(self._ioformat.format(ops=round(reads),
band=bytefmt(nread)))
for widget in widgets:
if widget.get("visited") is False:
widgets.remove(widget)
@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 ""

View file

@ -1,324 +0,0 @@
# pylint: disable=R0201
"""Output classes"""
import sys
import json
import uuid
import logging
import bumblebee.store
import bumblebee.util
MAX_PERCENTS = 100.
CHARS = 8
HBARS = [
u"\u2581",
u"\u2582",
u"\u2583",
u"\u2584",
u"\u2585",
u"\u2586",
u"\u2587",
u"\u2588"]
VBARS = [
u"\u258f",
u"\u258e",
u"\u258d",
u"\u258c",
u"\u258b",
u"\u258a",
u"\u2589",
u"\u2588"]
log = logging.getLogger(__name__)
def scrollable(func):
def wrapper(module, widget):
text = func(module, widget)
if not text:
return text
width = widget.get("theme.width", int(module.parameter("width", 30)))
if bumblebee.util.asbool(module.parameter("scrolling.makewide", "true")):
widget.set("theme.minwidth", "A"*width)
if width < 0:
return text
if len(text) <= width:
return text
# we need to shorten
try:
bounce = int(module.parameter("scrolling.bounce", 1))
except ValueError:
bounce = 1
try:
scroll_speed = int(module.parameter("scrolling.speed", 1))
except ValueError:
scroll_speed = 1
start = widget.get("scrolling.start", -1)
direction = widget.get("scrolling.direction", "right")
start += scroll_speed if direction == "right" else -(scroll_speed)
if width + start > len(text) + (scroll_speed -1):
if bounce:
widget.set("scrolling.direction", "left")
else:
start = 0
elif start <= 0:
if bounce:
widget.set("scrolling.direction", "right")
else:
start = len(text)
widget.set("scrolling.start", start)
text = text[start:width+start]
return text
return wrapper
class Bar(object):
"""superclass"""
bars = None
def __init__(self, value):
"""
Args:
value (float): value between 0. and 100. meaning percents
"""
self.value = value
class HBar(Bar):
"""horizontal bar (1 char)"""
bars = HBARS
def __init__(self, value):
"""
Args:
value (float): value between 0. and 100. meaning percents
"""
super(HBar, self).__init__(value)
self.step = MAX_PERCENTS / CHARS
def get_char(self):
"""
Decide which char to draw
Return: str
"""
for i in range(CHARS):
left = i * self.step
right = (i + 1) * self.step
if left <= self.value < right:
return self.bars[i]
return self.bars[-1]
def hbar(value):
"""wrapper function"""
return HBar(value).get_char()
class VBar(Bar):
"""vertical bar (can be more than 1 char)"""
bars = VBARS
def __init__(self, value, width=1):
"""
Args:
value (float): value between 0. and 100. meaning percents
width (int): width
"""
super(VBar, self).__init__(value)
self.step = MAX_PERCENTS / (CHARS * width)
self.width = width
def get_chars(self):
"""
Decide which char to draw
Return: str
"""
if self.value == 100:
return self.bars[-1] * self.width
if self.width == 1:
for i in range(CHARS):
left = i * self.step
right = (i + 1) * self.step
if left <= self.value < right:
return self.bars[i]
else:
full_parts = int(self.value // (self.step * CHARS))
remainder = self.value - full_parts * self.step * CHARS
empty_parts = self.width - full_parts
if remainder >= 0:
empty_parts -= 1
part_vbar = VBar(remainder * self.width) # scale to width
chars = self.bars[-1] * full_parts
chars += part_vbar.get_chars()
chars += " " * empty_parts
return chars
def vbar(value, width):
"""wrapper function"""
return VBar(value, width).get_chars()
class Widget(bumblebee.store.Store):
"""Represents a single visible block in the status bar"""
def __init__(self, full_text="", name=""):
super(Widget, self).__init__()
self._full_text = full_text
self.module = None
self._module = None
self._minimized = False
self.name = name
self.id = str(uuid.uuid4())
def get_module(self):
return self._module
def toggle_minimize(self):
self._minimized = not self._minimized
def link_module(self, module):
"""Set the module that spawned this widget
This is done outside the constructor to avoid having to
pass in the module name in every concrete module implementation"""
self.module = module.name
self._module = module
def cls(self):
if not self._module:
return None
return self._module.__module__.replace("bumblebee.modules.", "")
def state(self):
"""Return the widget's state"""
if self._module and hasattr(self._module, "state"):
states = self._module.state(self)
if not isinstance(states, list):
return [states]
return states
return []
def full_text(self, value=None):
"""Set or retrieve the full text to display in the widget"""
if value:
self._full_text = value
else:
if self._minimized:
return u"\u2026"
if callable(self._full_text):
return self._full_text(self)
else:
return self._full_text
class I3BarOutput(object):
"""Manage output according to the i3bar protocol"""
def __init__(self, theme, config=None):
self._theme = theme
self._widgets = []
self._started = False
self._config = config
def started(self):
return self._started
def start(self):
"""Print start preamble for i3bar protocol"""
self._started = True
sys.stdout.write(json.dumps({"version": 1, "click_events": True}) + "\n[\n")
def stop(self):
"""Finish i3bar protocol"""
sys.stdout.write("]\n")
def draw(self, widget, module=None, engine=None):
"""Draw a single widget"""
full_text = widget.full_text()
if widget.get_module() and widget.get_module().hidden():
return
if widget.get_module() and widget.get_module().name in self._config.autohide():
if not any(state in widget.state() for state in ["warning", "critical"]):
return
padding = self._theme.padding(widget)
prefix = self._theme.prefix(widget, padding)
suffix = self._theme.suffix(widget, padding)
if self._config.markup() == "pango":
# add prefix/suffix colors
fg = self._theme.prefix_fg(widget)
bg = self._theme.prefix_bg(widget)
prefix = "<span {} {}>{}</span>".format(
"foreground='{}'".format(fg) if fg else "",
"background='{}'".format(bg) if bg else "",
prefix
)
if prefix:
full_text = u"{}{}".format(prefix, full_text)
if suffix:
full_text = u"{}{}".format(full_text, suffix)
separator = self._theme.separator(widget)
if separator:
self._widgets.append({
u"full_text": separator,
"separator": False,
"color": self._theme.separator_fg(widget),
"background": self._theme.separator_bg(widget),
"separator_block_width": self._theme.separator_block_width(widget),
})
width = self._theme.minwidth(widget)
if width:
full_text = full_text.ljust(len(width) + len(prefix) + len(suffix))
markup = "none" if not self._config else self._config.markup()
if markup == "pango":
full_text = full_text.replace("&", "&amp;")
self._widgets.append({
u"full_text": full_text,
"color": self._theme.fg(widget),
"background": self._theme.bg(widget),
"separator_block_width": self._theme.separator_block_width(widget),
"separator": True if separator is None else False,
"min_width": None,
# "min_width": width + "A"*(len(prefix) + len(suffix)) if width else None,
"align": self._theme.align(widget),
"instance": widget.id,
"name": module.id,
"markup": markup,
})
def begin(self):
"""Start one output iteration"""
self._widgets = []
self._theme.reset()
def flush(self):
"""Flushes output"""
widgets = self._widgets
if self._config and self._config.reverse():
widgets = list(reversed(widgets))
sys.stdout.write(json.dumps(widgets))
if len(self._config.unused_keys()) > 0:
for key in self._config.unused_keys():
log.warning("unused parameter {} - please check the documentation of the affected module to ensure the parameter exists".format(key))
def end(self):
"""Finalizes output"""
sys.stdout.write(",\n")
sys.stdout.flush()
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,80 +0,0 @@
"""Pop-up menus."""
import logging
try:
import Tkinter as tk
except ImportError:
# python 3
try:
import tkinter as tk
except ImportError:
logging.warning("failed to import tkinter - bumblebee popups won't work!")
class PopupMenu:
"""The popup-menu."""
def __init__(self):
"""Initialize."""
# menu widget
self.root = tk.Tk()
self.root.withdraw()
self.menu = tk.Menu(self.root)
# internal state
self._item_count = 0
self._clicked_item = None
self._active = False
# bind event of popup getting closed by clicking outside of its area
self.menu.bind('<Unmap>',
lambda event: self.root.after_idle(
self._dismiss_callback))
def add_menuitem(self, menuitem, callback=None):
"""Add menu items."""
item_count = self._item_count
def click_callback():
# call internal callback with item index
self._item_callback(item_count)
# default to internal callback
if callback is None:
callback = click_callback
self.menu.add_command(label=menuitem,
command=callback)
# track item index
self._item_count += 1
def _item_callback(self, which_item):
"""Menu item click callback."""
logging.debug('popup: item callback: {}'.format(which_item))
self._clicked_item = which_item
self.root.destroy()
self._active = False
def _dismiss_callback(self):
"""Menu dismissed."""
logging.debug('popup: menu dismissed')
if self._active is True:
self._clicked_item = None
self.root.destroy()
def show(self, event):
"""Show popup."""
self._clicked_item = None
self.menu.tk_popup(event['x'], event['y']-50)
self._active = True
self.root.mainloop()
return self._clicked_item
def create_and_show_menu(event, *menuitems):
"""Create a menu object and show."""
menu_obj = PopupMenu()
for menuitem in menuitems:
menu_obj.add_menuitem(*menuitem)
return menu_obj.show(event)

View file

@ -1,56 +0,0 @@
"""Pop-up menus."""
import logging
try:
import Tkinter as tk
except ImportError:
# python 3
try:
import tkinter as tk
except ImportError:
logging.warning("failed to import tkinter - bumblebee popups won't work!")
import functools
class PopupMenu(object):
def __init__(self, parent=None, leave=True):
if not parent:
self._root = tk.Tk()
self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out)
else:
self._root = parent.root()
self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out)
if leave:
self._menu.bind("<Leave>", self._on_focus_out)
def root(self):
return self._root
def menu(self):
return self._menu
def _on_focus_out(self, event=None):
self._root.destroy()
def _on_click(self, callback):
self._root.destroy()
callback()
def add_cascade(self, menuitem, submenu):
self._menu.add_cascade(label=menuitem, menu=submenu.menu())
def add_menuitem(self, menuitem, callback):
self._menu.add_command(label=menuitem, command=functools.partial(self._on_click, callback))
def show(self, event, offset_x=0, offset_y=0):
try:
self._menu.tk_popup(event['x'] + offset_x, event['y'] + offset_y)
finally:
self._menu.grab_release()
self._root.mainloop()

View file

@ -1,27 +0,0 @@
"""Store interface
Allows arbitrary classes to offer a simple get/set
store interface by deriving from the Store class in
this module
"""
class Store(object):
"""Interface for storing and retrieving simple values"""
def __init__(self):
self._data = {}
self._unused = {}
def set(self, key, value):
"""Set 'key' to 'value', overwriting 'key' if it exists already"""
self._data[key] = value
self._unused[key] = value
def unused_keys(self):
return self._unused.keys()
def get(self, key, default=None):
"""Return the current value of 'key', or 'default' if 'key' is not set"""
self._unused.pop(key, None)
return self._data.get(key, default)
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,310 +0,0 @@
# pylint: disable=C0103
"""Theme support"""
import os
import glob
import copy
import json
import io
import re
import logging
try:
import requests
from requests.exceptions import RequestException
except ImportError:
pass
import bumblebee.error
def theme_path():
"""Return the path of the theme directory"""
return [
os.path.dirname("{}/../themes/".format(os.path.dirname(os.path.realpath(__file__)))),
os.path.dirname(os.path.expanduser("~/.config/bumblebee-status/themes/")),
]
def themes():
themes = {}
for path in theme_path():
for filename in glob.iglob("{}/*.json".format(path)):
if "test" not in filename:
themes[os.path.basename(filename).replace(".json", "")] = 1
result = list(themes.keys())
result.sort()
return result
class Theme(object):
"""Represents a collection of icons and colors"""
def __init__(self, name, iconset="auto"):
self._widget = None
self._cycle_idx = 0
self._cycle = {}
self._prevbg = None
self._colorset = {}
self._iconset = iconset
self.load_symbols()
data = self.load(name)
if not data:
raise bumblebee.error.ThemeLoadError("no such theme")
self._init(data)
def load_symbols(self):
self._symbols = {}
path = os.path.expanduser("~/.config/bumblebee-status/")
try:
os.makedirs(path)
except Exception:
pass
try:
if os.path.exists("{}/symbols.json".format(path)):
data = json.load(io.open("{}/symbols.json".format(path)))
self._symbols = {}
for icon in data["icons"]:
code = int(icon["unicode"], 16)
try:
code = unichr(code)
except Exception:
code = chr(code)
self._symbols["${{{}}}".format(icon["id"])] = code
self._symbols["${{{}}}".format(icon["name"])] = code
except Exception as e:
logging.error("failed to load symbols: {}".format(str(e)))
def _init(self, data):
"""Initialize theme from data structure"""
self._theme = data
if self._iconset != "auto":
self._merge(data, self._load_icons(self._iconset))
else:
for iconset in data.get("icons", []):
self._merge(data, self._load_icons(iconset))
for colorset in data.get("colors", []):
self._merge(self._colorset, self._load_colors(colorset))
self._defaults = data.get("defaults", {})
self._cycles = self._theme.get("cycle", [])
self.reset()
def data(self):
"""Return the raw theme data"""
return self._theme
def reset(self):
"""Reset theme to initial state"""
self._cycle = self._cycles[0] if len(self._cycles) > 0 else {}
self._cycle_idx = 0
self._widget = None
self._prevbg = None
def icon(self, widget):
icon = self._get(widget, "icon", None)
if icon is None:
return self._get(widget, "prefix", None)
def get(self, widget, attribute, default_value=""):
return self._get(widget, attribute, default_value)
def padding(self, widget):
"""Return padding for widget"""
return self._get(widget, "padding", "")
def prefix(self, widget, default=None):
"""Return the theme prefix for a widget's full text"""
padding = self.padding(widget)
pre = self._get(widget, "prefix", None)
return u"{}{}{}".format(padding, pre, padding) if pre else default
def prefix_fg(self, widget):
"""Return the foreground color for the prefix"""
return self._get(widget, "prefixfg", None)
def prefix_bg(self, widget):
"""Return the background color for the prefix"""
return self._get(widget, "prefixbg", None)
def suffix_fg(self, widget):
"""Return the foreground color for the suffix"""
return self._get(widget, "suffixfg", None)
def suffix_bg(self, widget):
"""Return the background color for the suffix"""
return self._get(widget, "suffixbg", None)
def symbol(self, widget, name, default=None):
return self._get(widget, name, default)
def suffix(self, widget, default=None):
"""Return the theme suffix for a widget's full text"""
padding = self._get(widget, "padding", "")
suf = self._get(widget, "suffix", None)
return u"{}{}{}".format(padding, suf, padding) if suf else default
def fg(self, widget):
"""Return the foreground color for this widget"""
return self._get(widget, "fg", None)
def bg(self, widget):
"""Return the background color for this widget"""
return self._get(widget, "bg", None)
def align(self, widget):
"""Return the widget alignment"""
return self._get(widget, "align", None)
def minwidth(self, widget):
"""Return the minimum width string for this widget"""
return self._get(widget, "minwidth", "")
def separator(self, widget):
"""Return the separator between widgets"""
return self._get(widget, "separator", None)
def separator_fg(self, widget):
"""Return the separator's foreground/text color"""
return self.bg(widget)
def separator_bg(self, widget):
"""Return the separator's background color"""
return self._prevbg
def separator_block_width(self, widget):
"""Return the SBW"""
return self._get(widget, "separator-block-width", None)
def _load_wal_colors(self):
walfile = os.path.expanduser("~/.cache/wal/colors.json")
result = {}
with io.open(walfile) as data:
colors = json.load(data)
for field in ["special", "colors"]:
for key in colors[field]:
result[key] = colors[field][key]
return result
def _load_colors(self, name):
"""Load colors for a theme"""
try:
if name == "wal":
return self._load_wal_colors()
except Exception as e:
logging.error("failed to load colors: {}".format(str(e)))
def _load_icons(self, name):
"""Load icons for a theme"""
result = {}
for path in theme_path():
self._merge(result, self.load(name, path="{}/icons/".format(path)))
return self._replace_symbols(result)
def _replace_symbols(self, data):
rep = json.dumps(data)
tokens = re.findall(r"\${[^}]+}", rep)
for token in tokens:
rep = rep.replace(token, self._symbols[token])
return json.loads(rep)
def load(self, name, path=theme_path()):
"""Load and parse a theme file"""
result = None
full_name = os.path.expanduser(name)
if os.path.isfile(full_name):
path = os.path.dirname(full_name)
name = os.path.basename(full_name)
name,_,_ = name.rpartition(".json")
return self.load(name, path)
if not isinstance(path, list):
path = [path]
for p in path:
themefile = "{}/{}.json".format(p, name)
if os.path.isfile(themefile):
try:
with io.open(themefile, encoding="utf-8") as data:
if result is None:
result = json.load(data)
else:
self._merge(result, json.load(data))
except ValueError as exception:
raise bumblebee.error.ThemeLoadError("JSON error: {}".format(exception))
return result
def _get(self, widget, name, default=None):
"""Return the config value 'name' for 'widget'"""
if not self._widget:
self._widget = widget
if name in bumblebee.util.aslist(self._widget.get("theme.exclude", "")):
return None
if self._widget != widget:
self._prevbg = self.bg(self._widget)
self._widget = widget
if len(self._cycles) > 0:
self._cycle_idx = (self._cycle_idx + 1) % len(self._cycles)
self._cycle = self._cycles[self._cycle_idx]
module_theme = self._theme.get(widget.module, {})
class_theme = self._theme.get(widget.cls(), {})
state_themes = []
# avoid infinite recursion
states = widget.state()
if name not in states:
for state in states:
if state:
state_themes.append(self._get(widget, state, {}))
value = self._defaults.get(name, default)
value = widget.get("theme.{}".format(name), value)
value = self._cycle.get(name, value)
value = class_theme.get(name, value)
value = module_theme.get(name, value)
for theme in state_themes:
value = theme.get(name, value)
if isinstance(value, list):
key = "{}-idx".format(name)
idx = widget.get(key, 0)
widget.set(key, (idx + 1) % len(value))
value = value[idx]
mod = widget.get_module()
if mod and not mod.parameter("is-unittest"):
value = widget.get_module().parameter("theme.{}".format(name), value)
if isinstance(value, list) or isinstance(value, dict):
return value
return self._colorset.get(value, value)
# algorithm copied from
# http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts
# nicely done :)
def _merge(self, target, *args):
"""Merge two arbitrarily nested data structures"""
if len(args) > 1:
for item in args:
self._merge(item)
return target
item = args[0]
if not isinstance(item, dict):
return item
for key, value in item.items():
if key in target and isinstance(target[key], dict):
self._merge(target[key], value)
else:
if not key in target:
target[key] = copy.deepcopy(value)
return target
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
import shlex
import logging
import subprocess
try:
from exceptions import RuntimeError
except ImportError:
# Python3 doesn't require this anymore
pass
def asbool(val):
if val is None:
return False
if isinstance(val, bool):
return val
val = str(val).strip().lower()
return val in ("t", "true", "y", "yes", "on", "1")
def aslist(val):
if val is None:
return []
if isinstance(val, list):
return val
return str(val).replace(' ', '').split(',')
def execute(cmd, wait=True):
logging.info("executing command '{}'".format(cmd))
args = shlex.split(cmd)
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
rv = None
if wait:
out, _ = proc.communicate()
if proc.returncode != 0:
raise RuntimeError("{} exited with {}".format(cmd, proc.returncode))
if hasattr(out, "decode"):
rv = out.decode("utf-8", "ignore")
else:
rv = out
logging.info(u"command returned '{}'".format("" if not rv else rv))
return rv
def bytefmt(num, fmt="{:.2f}"):
"""
format a value of bytes to a more human readable pattern
example: 15 * 1024 becomes 15KiB
Args:
num (int): bytes
fmt (string): format
Return: string
"""
for unit in ["", "Ki", "Mi", "Gi"]:
if num < 1024.0:
return "{}{}B".format(fmt, unit).format(num)
num /= 1024.0
return "{}GiB".format(fmt).format(num*1024.0)
def durationfmt(duration, shorten=False, suffix=False):
duration = int(duration)
minutes, seconds = divmod(duration, 60)
hours, minutes = divmod(minutes, 60)
suf = "m"
res = "{:02d}:{:02d}".format(minutes, seconds)
if hours > 0:
if shorten:
res = "{:02d}:{:02d}".format(hours, minutes)
else:
res = "{:02d}:{}".format(hours, res)
suf = "h"
return "{}{}".format(res, suf if suffix else "")
def which(program):
import os
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
localPATH = os.environ["PATH"].split(os.pathsep)
localPATH += ["/sbin", "/usr/sbin/", "/usr/local/sbin"]
for path in localPATH:
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

16
core/output.py Normal file
View file

@ -0,0 +1,16 @@
import json
class i3(object):
def start(self):
return '{}\n'.format(json.dumps({ 'version': 1, 'click_events': True }))
def stop(self):
return ']\n'
def begin_status_line(self):
return '['
def end_status_line(self):
return '],\n'
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -1,3 +0,0 @@
#!/bin/sh
find . -name "*.py"|xargs pylint --disable=R0903,R0201,C0330

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