From f306366629a661703de7cb83f1faf2957eb8902b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 2 Dec 2016 17:52:05 +0100 Subject: [PATCH 001/104] [core] Minor refactoring Use a small helper function from util, tidy up some parts of the output. --- bumblebee/output.py | 12 +++++------- bumblebee/outputs/i3.py | 8 ++------ bumblebee/theme.py | 8 ++++---- bumblebee/util.py | 13 +++++++------ 4 files changed, 18 insertions(+), 23 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index 1f494ad..cfdc88b 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -1,8 +1,7 @@ import os -import shlex import inspect import threading -import subprocess +import bumblebee.util def output(args): import bumblebee.outputs.i3 @@ -57,8 +56,7 @@ class Command(object): cmd(self._event, self._widget) else: c = cmd.format(*args, **kwargs) - DEVNULL = open(os.devnull, 'wb') - subprocess.Popen(shlex.split(c), stdout=DEVNULL, stderr=DEVNULL) + bumblebee.util.execute(c, False) class Output(object): def __init__(self, config): @@ -103,14 +101,14 @@ class Output(object): def wait(self): self._wait.wait(self._config.parameter("interval", 1)) - def start(self): - pass - def draw(self, widgets, theme): if not type(widgets) is list: widgets = [ widgets ] self._draw(widgets, theme) + def start(self): + pass + def _draw(self, widgets, theme): pass diff --git a/bumblebee/outputs/i3.py b/bumblebee/outputs/i3.py index c0800dd..e287c6c 100644 --- a/bumblebee/outputs/i3.py +++ b/bumblebee/outputs/i3.py @@ -51,10 +51,6 @@ class Output(bumblebee.output.Output): "separator_block_width": 0, }) - sep = theme.default_separators(widget) - sep = sep if sep else False - width = theme.separator_block_width(widget) - width = width if width else 0 self._data.append({ u"full_text": " {} {} {}".format( theme.prefix(widget), @@ -65,8 +61,8 @@ class Output(bumblebee.output.Output): "background": theme.background(widget), "name": widget.module(), "instance": widget.instance(), - "separator": sep, - "separator_block_width": width, + "separator": theme.default_separators(widget, False), + "separator_block_width": theme.separator_block_width(widget, 0), }) theme.next_widget() diff --git a/bumblebee/theme.py b/bumblebee/theme.py index cd8d860..2f7d5c7 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -89,8 +89,8 @@ class Theme: def separator(self, widget): return self._get(widget, "separator") - def default_separators(self, widget): - return self._get(widget, "default-separators") + def default_separators(self, widget, default): + return self._get(widget, "default-separators", default) def separator_color(self, widget): return self.background(widget) @@ -98,8 +98,8 @@ class Theme: def separator_background(self, widget): return self._background[1] - def separator_block_width(self, widget): - return self._get(widget, "separator-block-width") + def separator_block_width(self, widget, default): + return self._get(widget, "separator-block-width", default) def _get(self, widget, name, default = None): module = widget.module() diff --git a/bumblebee/util.py b/bumblebee/util.py index c38bb79..3a6a60c 100644 --- a/bumblebee/util.py +++ b/bumblebee/util.py @@ -21,12 +21,13 @@ def durationfmt(duration): return res -def execute(cmd): +def execute(cmd, wait=True): args = shlex.split(cmd) p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - out, err = p.communicate() - if p.returncode != 0: - raise RuntimeError("{} exited with {}".format(cmd, p.returncode)) - - return out.decode("utf-8") + if wait: + out, err = p.communicate() + if p.returncode != 0: + raise RuntimeError("{} exited with {}".format(cmd, p.returncode)) + return out.decode("utf-8") + return None From 2f3f171ca5311332f3fd7d5fda090f3fea7ca019 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 2 Dec 2016 18:53:34 +0100 Subject: [PATCH 002/104] [core] Remove alias from module Hide alias concept for modules in the engine. That way, the individual modules never get to know about whether a module has been aliased or not. see #23 --- bumblebee/config.py | 7 +++++-- bumblebee/engine.py | 6 +++++- bumblebee/module.py | 10 ++++------ bumblebee/modules/battery.py | 4 ++-- bumblebee/modules/brightness.py | 4 ++-- bumblebee/modules/caffeine.py | 4 ++-- bumblebee/modules/cmus.py | 4 ++-- bumblebee/modules/cpu.py | 4 ++-- bumblebee/modules/disk.py | 4 ++-- bumblebee/modules/dnf.py | 4 ++-- bumblebee/modules/layout.py | 4 ++-- bumblebee/modules/load.py | 4 ++-- bumblebee/modules/memory.py | 4 ++-- bumblebee/modules/nic.py | 4 ++-- bumblebee/modules/pacman.py | 4 ++-- bumblebee/modules/ping.py | 4 ++-- bumblebee/modules/pulseaudio.py | 4 ++-- bumblebee/modules/spacer.py | 4 ++-- bumblebee/modules/time.py | 4 ++-- bumblebee/modules/xrandr.py | 4 ++-- bumblebee/outputs/i3.py | 2 -- 21 files changed, 48 insertions(+), 45 deletions(-) diff --git a/bumblebee/config.py b/bumblebee/config.py index 501393d..57958ac 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -35,10 +35,13 @@ class print_usage(argparse.Action): print("") class ModuleConfig(object): - def __init__(self, config, prefix): - self._prefix = prefix + def __init__(self, config, name, alias): + self._prefix = alias if alias else name self._config = config + def prefix(self): + return self._prefix + def set(self, name, value): name = self._prefix + name return self._config.set(name, value) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 1625ee7..cc0b7b9 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -1,6 +1,7 @@ import importlib import bumblebee.theme import bumblebee.output +import bumblebee.config import bumblebee.modules class Engine: @@ -13,7 +14,10 @@ class Engine: def load_module(self, modulespec): name = modulespec["name"] module = importlib.import_module("bumblebee.modules.{}".format(name)) - return getattr(module, "Module")(self._output, self._config, modulespec["alias"]) + cfg = bumblebee.config.ModuleConfig(self._config, name, modulespec["alias"]) + obj = getattr(module, "Module")(self._output, cfg) + obj.register_callbacks() + return obj def load_modules(self): for m in self._config.modules(): diff --git a/bumblebee/module.py b/bumblebee/module.py index 6152777..0f497a2 100644 --- a/bumblebee/module.py +++ b/bumblebee/module.py @@ -2,7 +2,6 @@ import os import pkgutil import importlib -import bumblebee.config import bumblebee.modules def modules(): @@ -27,12 +26,11 @@ class ModuleDescription(object): return getattr(self._mod, "parameters", lambda: [ "n/a" ])() class Module(object): - def __init__(self, output, config, alias=None): + def __init__(self, output, config): self._output = output - self._alias = alias - name = "{}.".format(alias if alias else self.__module__.split(".")[-1]) - self._config = bumblebee.config.ModuleConfig(config, name) + self._config = config + def register_callbacks(self): buttons = [ { "name": "left-click", "id": 1 }, { "name": "middle-click", "id": 2 }, @@ -58,6 +56,6 @@ class Module(object): return "default" def instance(self, widget=None): - return self._alias if self._alias else self.__module__.split(".")[-1] + return self._config.prefix() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index 2a570df..8215693 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -9,8 +9,8 @@ def parameters(): return [ "battery.device: The device to read from (defaults to BAT0)" ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._battery = config.parameter("device", "BAT0") self._capacity = 100 self._status = "Unknown" diff --git a/bumblebee/modules/brightness.py b/bumblebee/modules/brightness.py index 114c11e..9227ce8 100644 --- a/bumblebee/modules/brightness.py +++ b/bumblebee/modules/brightness.py @@ -9,8 +9,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._brightness = 0 self._max = 0 self._percent = 0 diff --git a/bumblebee/modules/caffeine.py b/bumblebee/modules/caffeine.py index 39f20d5..d7c2dad 100644 --- a/bumblebee/modules/caffeine.py +++ b/bumblebee/modules/caffeine.py @@ -7,8 +7,8 @@ def description(): return "Enable/disable auto screen lock." class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._activated = 0 output.add_callback(module="caffeine.activate", button=1, cmd=[ 'notify-send "Consuming caffeine"', 'xset s off' ]) output.add_callback(module="caffeine.deactivate", button=1, cmd=[ 'notify-send "Out of coffee"', 'xset s default' ]) diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index b847ad2..de42ecf 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -15,8 +15,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._status = "default" self._fmt = self._config.parameter("format", "{artist} - {title} {position}/{duration}") diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index b156b37..619019b 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -11,8 +11,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._perc = psutil.cpu_percent(percpu=False) output.add_callback(module=self.instance(), button=1, cmd="gnome-system-monitor") diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py index 679e271..67a79e1 100644 --- a/bumblebee/modules/disk.py +++ b/bumblebee/modules/disk.py @@ -12,8 +12,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._path = self._config.parameter("path", "/") output.add_callback(module=self.instance(), button=1, cmd="nautilus {}".format(self._path)) diff --git a/bumblebee/modules/dnf.py b/bumblebee/modules/dnf.py index ead5065..4101a8c 100644 --- a/bumblebee/modules/dnf.py +++ b/bumblebee/modules/dnf.py @@ -59,8 +59,8 @@ def get_dnf_info(obj): obj.set("other", other) class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._counter = {} self._thread = threading.Thread(target=get_dnf_info, args=(self,)) diff --git a/bumblebee/modules/layout.py b/bumblebee/modules/layout.py index 084d27d..b317655 100644 --- a/bumblebee/modules/layout.py +++ b/bumblebee/modules/layout.py @@ -13,8 +13,8 @@ def parameters(): class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._languages = self._config.parameter("lang", "en").split("|") self._idx = 0 diff --git a/bumblebee/modules/load.py b/bumblebee/modules/load.py index fba5dbd..aad9dfe 100644 --- a/bumblebee/modules/load.py +++ b/bumblebee/modules/load.py @@ -12,8 +12,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._cpus = 1 try: self._cpus = multiprocessing.cpu_count() diff --git a/bumblebee/modules/memory.py b/bumblebee/modules/memory.py index 70b10b2..d38ac0f 100644 --- a/bumblebee/modules/memory.py +++ b/bumblebee/modules/memory.py @@ -12,8 +12,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._mem = psutil.virtual_memory() output.add_callback(module=self.instance(), button=1, cmd="gnome-system-monitor") diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py index 94f8bc4..64483c5 100644 --- a/bumblebee/modules/nic.py +++ b/bumblebee/modules/nic.py @@ -10,8 +10,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._exclude = tuple(filter(len, self._config.parameter("exclude", "lo,virbr,docker,vboxnet,veth").split(","))) self._state = "down" self._typecache = {} diff --git a/bumblebee/modules/pacman.py b/bumblebee/modules/pacman.py index 66ece38..3ecb5db 100644 --- a/bumblebee/modules/pacman.py +++ b/bumblebee/modules/pacman.py @@ -6,8 +6,8 @@ def description(): return "Displays available updates per repository for pacman." class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._count = 0 def widgets(self): diff --git a/bumblebee/modules/ping.py b/bumblebee/modules/ping.py index 20fec39..b523788 100644 --- a/bumblebee/modules/ping.py +++ b/bumblebee/modules/ping.py @@ -56,8 +56,8 @@ def get_rtt(obj): class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._counter = {} diff --git a/bumblebee/modules/pulseaudio.py b/bumblebee/modules/pulseaudio.py index a8cda45..079e07c 100644 --- a/bumblebee/modules/pulseaudio.py +++ b/bumblebee/modules/pulseaudio.py @@ -18,8 +18,8 @@ def parameters(): class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._module = self.__module__.split(".")[-1] self._left = 0 diff --git a/bumblebee/modules/spacer.py b/bumblebee/modules/spacer.py index 798338e..f9b88d3 100644 --- a/bumblebee/modules/spacer.py +++ b/bumblebee/modules/spacer.py @@ -8,8 +8,8 @@ def parameters(): return [ "spacer.text: Text to draw (defaults to '')" ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) def widgets(self): return bumblebee.output.Widget(self, self._config.parameter("text", "")) diff --git a/bumblebee/modules/time.py b/bumblebee/modules/time.py index 4a19649..68f1beb 100644 --- a/bumblebee/modules/time.py +++ b/bumblebee/modules/time.py @@ -21,8 +21,8 @@ def default_format(module): return default class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) module = self.__module__.split(".")[-1] diff --git a/bumblebee/modules/xrandr.py b/bumblebee/modules/xrandr.py index 51998a9..7817143 100644 --- a/bumblebee/modules/xrandr.py +++ b/bumblebee/modules/xrandr.py @@ -13,8 +13,8 @@ def parameters(): ] class Module(bumblebee.module.Module): - def __init__(self, output, config, alias): - super(Module, self).__init__(output, config, alias) + def __init__(self, output, config): + super(Module, self).__init__(output, config) self._widgets = [] diff --git a/bumblebee/outputs/i3.py b/bumblebee/outputs/i3.py index e287c6c..d9d1894 100644 --- a/bumblebee/outputs/i3.py +++ b/bumblebee/outputs/i3.py @@ -14,8 +14,6 @@ def read_input(output): if line == "[": continue if line == "]": break - DEVNULL = open(os.devnull, 'wb') - event = json.loads(line) cb = output.callback(event) if cb: From 31067159d604f40fcaa0ca08468056f263bee7f9 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 2 Dec 2016 19:06:47 +0100 Subject: [PATCH 003/104] [modules/nic] Minor refactoring Remove impractical cache. --- bumblebee/modules/nic.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py index 64483c5..0db9e3a 100644 --- a/bumblebee/modules/nic.py +++ b/bumblebee/modules/nic.py @@ -13,8 +13,6 @@ class Module(bumblebee.module.Module): def __init__(self, output, config): super(Module, self).__init__(output, config) self._exclude = tuple(filter(len, self._config.parameter("exclude", "lo,virbr,docker,vboxnet,veth").split(","))) - self._state = "down" - self._typecache = {} def widgets(self): result = [] @@ -50,12 +48,10 @@ class Module(bumblebee.module.Module): def state(self, widget): intf = widget.get("intf") - if not intf in self._typecache: - t = "wireless" if self._iswlan(intf) else "wired" - t = "tunnel" if self._istunnel(intf) else t - self._typecache[intf] = t + iftype = "wireless" if self._iswlan(intf) else "wired" + iftype = "tunnel" if self._istunnel(intf) else iftype - return "{}-{}".format(self._typecache[intf], widget.get("state")) + return "{}-{}".format(iftype, widget.get("state")) def warning(self, widget): return widget.get("state") != "up" From 20858991b97f58f41e5cd020a64f624a46ce8369 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 2 Dec 2016 22:35:28 +0100 Subject: [PATCH 004/104] [theme] Fix cycling through widget styles Cycled widget styles (such as the battery charging style) were broken until now. The reason for this: They maintain state that represents the current cycle position (i.e. what is the current icon that is being displayed), but that is done in a way that uses repr() on the widget object. Since the widget objects are re-created each time the bar is drawn, this is a deeply flawed design. Instead, use the instance() of the widget for now. --- bumblebee/config.py | 4 ++++ bumblebee/theme.py | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bumblebee/config.py b/bumblebee/config.py index 57958ac..49005ad 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -78,6 +78,10 @@ class Config(object): return self._store.get(name, default) def increase(self, name, limit, default): + if not name in self._store: + self._store[name] = default + return default + self._store[name] += 1 if self._store[name] >= limit: self._store[name] = default diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 2f7d5c7..42e6c01 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -120,9 +120,8 @@ class Theme: value = instance_state_theme.get(name, value) if type(value) is list: - key = "{}{}".format(repr(widget), value) - idx = self._config.parameter(key, 0) - self._config.increase(key, len(value), 0) + key = "{}{}".format(widget.instance(), value) + idx = self._config.increase(key, len(value), 0) value = value[idx] return value if value else default From a8a6c9bba2d5719e80d49f2eac215419e412af24 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 3 Dec 2016 20:38:54 +0100 Subject: [PATCH 005/104] [core] Refactor engine This is going to be a bit more comprehensive than anticipated. In order to cleanly refactor the core and the engine, basically start from scratch with the implementation. Goals: * Test coverage * Maintain backwards compatibility with module interface as much as possible (but still make modules easier to code) * Simplicity see #23 --- README.md | 82 ------------- bin/customupdates | 21 ---- bin/load-i3-bars.sh | 26 ---- bin/toggle-display.sh | 12 -- bumblebee-status | 18 --- bumblebee/config.py | 120 ++---------------- bumblebee/engine.py | 38 ------ bumblebee/module.py | 61 ---------- bumblebee/modules/__init__.py | 0 bumblebee/modules/battery.py | 52 -------- bumblebee/modules/brightness.py | 33 ----- bumblebee/modules/caffeine.py | 38 ------ bumblebee/modules/cmus.py | 78 ------------ bumblebee/modules/cpu.py | 30 ----- bumblebee/modules/date.py | 1 - bumblebee/modules/datetime.py | 1 - bumblebee/modules/disk.py | 40 ------ bumblebee/modules/dnf.py | 98 --------------- bumblebee/modules/layout.py | 67 ----------- bumblebee/modules/load.py | 37 ------ bumblebee/modules/memory.py | 38 ------ bumblebee/modules/nic.py | 62 ---------- bumblebee/modules/pacman.py | 58 --------- bumblebee/modules/pasink.py | 1 - bumblebee/modules/pasource.py | 1 - bumblebee/modules/ping.py | 97 --------------- bumblebee/modules/pulseaudio.py | 92 -------------- bumblebee/modules/spacer.py | 17 --- bumblebee/modules/time.py | 34 ------ bumblebee/modules/xrandr.py | 89 -------------- bumblebee/output.py | 121 ------------------- bumblebee/outputs/__init__.py | 0 bumblebee/outputs/i3.py | 76 ------------ bumblebee/theme.py | 129 -------------------- bumblebee/util.py | 33 ----- screenshots/battery.png | Bin 1733 -> 0 bytes screenshots/brightness.png | Bin 1323 -> 0 bytes screenshots/caffeine.png | Bin 1143 -> 0 bytes screenshots/cmus.png | Bin 5031 -> 0 bytes screenshots/cpu.png | Bin 1811 -> 0 bytes screenshots/date.png | Bin 2877 -> 0 bytes screenshots/datetime.png | Bin 4889 -> 0 bytes screenshots/disk.png | Bin 3721 -> 0 bytes screenshots/dnf.png | Bin 1113 -> 0 bytes screenshots/load.png | Bin 2005 -> 0 bytes screenshots/memory.png | Bin 3603 -> 0 bytes screenshots/nic.png | Bin 2474 -> 0 bytes screenshots/pacman.png | Bin 860 -> 0 bytes screenshots/pasink.png | Bin 1486 -> 0 bytes screenshots/pasource.png | Bin 1505 -> 0 bytes screenshots/ping.png | Bin 1655 -> 0 bytes screenshots/pulseaudio.png | Bin 2495 -> 0 bytes screenshots/spacer.png | Bin 521 -> 0 bytes screenshots/themes/default.png | Bin 7251 -> 0 bytes screenshots/themes/powerline-gruvbox.png | Bin 11594 -> 0 bytes screenshots/themes/powerline-solarized.png | Bin 11991 -> 0 bytes screenshots/themes/powerline.png | Bin 10701 -> 0 bytes screenshots/themes/solarized.png | Bin 9612 -> 0 bytes screenshots/time.png | Bin 2456 -> 0 bytes screenshots/xrandr.png | Bin 2693 -> 0 bytes tests/__init__.py | 0 tests/modules/__init__.py | 0 tests/modules/test_cpu.py | 26 ---- tests/test_config.py | 14 ++- themes/default.json | 7 -- themes/gruvbox-powerline.json | 44 ------- themes/icons/ascii.json | 119 ------------------ themes/icons/awesome-fonts.json | 134 --------------------- themes/icons/paxy97.json | 5 - themes/powerline.json | 41 ------- themes/solarized-powerline.json | 42 ------- themes/solarized.json | 41 ------- 72 files changed, 19 insertions(+), 2155 deletions(-) delete mode 100644 README.md delete mode 100755 bin/customupdates delete mode 100755 bin/load-i3-bars.sh delete mode 100755 bin/toggle-display.sh mode change 100755 => 100644 bumblebee-status delete mode 100644 bumblebee/engine.py delete mode 100644 bumblebee/module.py delete mode 100644 bumblebee/modules/__init__.py delete mode 100644 bumblebee/modules/battery.py delete mode 100644 bumblebee/modules/brightness.py delete mode 100644 bumblebee/modules/caffeine.py delete mode 100644 bumblebee/modules/cmus.py delete mode 100644 bumblebee/modules/cpu.py delete mode 120000 bumblebee/modules/date.py delete mode 120000 bumblebee/modules/datetime.py delete mode 100644 bumblebee/modules/disk.py delete mode 100644 bumblebee/modules/dnf.py delete mode 100644 bumblebee/modules/layout.py delete mode 100644 bumblebee/modules/load.py delete mode 100644 bumblebee/modules/memory.py delete mode 100644 bumblebee/modules/nic.py delete mode 100644 bumblebee/modules/pacman.py delete mode 120000 bumblebee/modules/pasink.py delete mode 120000 bumblebee/modules/pasource.py delete mode 100644 bumblebee/modules/ping.py delete mode 100644 bumblebee/modules/pulseaudio.py delete mode 100644 bumblebee/modules/spacer.py delete mode 100644 bumblebee/modules/time.py delete mode 100644 bumblebee/modules/xrandr.py delete mode 100644 bumblebee/output.py delete mode 100644 bumblebee/outputs/__init__.py delete mode 100644 bumblebee/outputs/i3.py delete mode 100644 bumblebee/theme.py delete mode 100644 bumblebee/util.py delete mode 100644 screenshots/battery.png delete mode 100644 screenshots/brightness.png delete mode 100644 screenshots/caffeine.png delete mode 100644 screenshots/cmus.png delete mode 100644 screenshots/cpu.png delete mode 100644 screenshots/date.png delete mode 100644 screenshots/datetime.png delete mode 100644 screenshots/disk.png delete mode 100644 screenshots/dnf.png delete mode 100644 screenshots/load.png delete mode 100644 screenshots/memory.png delete mode 100644 screenshots/nic.png delete mode 100644 screenshots/pacman.png delete mode 100644 screenshots/pasink.png delete mode 100644 screenshots/pasource.png delete mode 100644 screenshots/ping.png delete mode 100644 screenshots/pulseaudio.png delete mode 100644 screenshots/spacer.png delete mode 100644 screenshots/themes/default.png delete mode 100644 screenshots/themes/powerline-gruvbox.png delete mode 100644 screenshots/themes/powerline-solarized.png delete mode 100644 screenshots/themes/powerline.png delete mode 100644 screenshots/themes/solarized.png delete mode 100644 screenshots/time.png delete mode 100644 screenshots/xrandr.png delete mode 100644 tests/__init__.py delete mode 100644 tests/modules/__init__.py delete mode 100644 tests/modules/test_cpu.py delete mode 100644 themes/default.json delete mode 100644 themes/gruvbox-powerline.json delete mode 100644 themes/icons/ascii.json delete mode 100644 themes/icons/awesome-fonts.json delete mode 100644 themes/icons/paxy97.json delete mode 100644 themes/powerline.json delete mode 100644 themes/solarized-powerline.json delete mode 100644 themes/solarized.json diff --git a/README.md b/README.md deleted file mode 100644 index d039179..0000000 --- a/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# bumblebee-status - -bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). - -Focus is on: -* Ease of use (no configuration files!) -* Theme support -* Extensibility (of course...) - -I hope you like it and appreciate any kind of feedback: Bug reports, Feature requests, etc. :) - -Thanks a lot! - -# Documentation -See [the wiki](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki) for documentation. - -Other resources: - -* A list of [available modules](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/Available-Modules) -* [How to write a theme](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/How-to-write-a-theme) -* [How to write a module](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/How-to-write-a-module) - -# Installation -``` -$ git clone git://github.com/tobi-wan-kenobi/bumblebee-status -``` - -# Usage - -Next, open your i3wm configuration and modify the *status_command* for your i3bar like this: - -``` -bar { - status_command = -m -p -t -} -``` - -You can retrieve a list of modules and themes by entering: -``` -$ cd bumblebee-status -$ ./bumblebee-status -l themes -$ ./bumblebee-status -l modules -``` - -As a simple example, this is what my i3 configuration looks like: - -``` -bar { - font pango:Inconsolata 10 - position top - tray_output none - status_command ~/.i3/bumblebee-status/bumblebee-status -m nic disk:/ cpu memory battery date time pasink pasource dnf -p time.format="%H:%M CW %V" date.format="%a, %b %d %Y" -t solarized-powerline -} - -``` - - -Restart i3wm and - that's it! - - -# Examples -Here are some screenshots for all themes that currently exist: - -Gruvbox Powerline (`-t gruvbox-powerline`) (contributed by [@paxy97](https://github.com/paxy97)): - -![Gruvbox Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-gruvbox.png) - -Solarized Powerline (`-t solarized-powerline`): - -![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png) - -Solarized (`-t solarized`): - -![Solarized](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/solarized.png) - -Powerline (`-t powerline`): - -![Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline.png) - -Default (nothing or `-t default`): - -![Default](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/default.png) diff --git a/bin/customupdates b/bin/customupdates deleted file mode 100755 index 3176c9d..0000000 --- a/bin/customupdates +++ /dev/null @@ -1,21 +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 diff --git a/bin/load-i3-bars.sh b/bin/load-i3-bars.sh deleted file mode 100755 index cbe0e95..0000000 --- a/bin/load-i3-bars.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -if [ ! -f ~/.i3/config.template ]; then - cp ~/.i3/config ~/.i3/config.template -else - cp ~/.i3/config.template ~/.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 ~/.i3/config.$screen-$others ]; then - cat ~/.i3/config.$screen-$others >> ~/.i3/config - else - if [ -f ~/.i3/config.$screen ]; then - cat ~/.i3/config.$screen >> ~/.i3/config - fi - fi -done <<< "$screens" - -i3-msg restart diff --git a/bin/toggle-display.sh b/bin/toggle-display.sh deleted file mode 100755 index bd13a29..0000000 --- a/bin/toggle-display.sh +++ /dev/null @@ -1,12 +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 - $i3bar_update -fi diff --git a/bumblebee-status b/bumblebee-status old mode 100755 new mode 100644 index 42258a4..e69de29 --- a/bumblebee-status +++ b/bumblebee-status @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -import sys -import bumblebee.config -import bumblebee.engine - -def main(): - config = bumblebee.config.Config(sys.argv[1:]) - - engine = bumblebee.engine.Engine(config) - engine.load_modules() - - engine.run() - -if __name__ == "__main__": - main() - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/config.py b/bumblebee/config.py index 49005ad..d9c9419 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -1,123 +1,19 @@ -import os import argparse -import textwrap -import bumblebee.theme -import bumblebee.module - -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 = " "*4 - - def __call__(self, parser, namespace, value, option_string=None): - if value == "modules": - self.print_modules() - elif value == "themes": - self.print_themes() - else: - parser.print_help() - parser.exit() - - def print_themes(self): - print(textwrap.fill(", ".join(bumblebee.theme.themes()), - 80, initial_indent = self._indent, subsequent_indent = self._indent - )) - - def print_modules(self): - for m in bumblebee.module.modules(): - print(textwrap.fill("{}: {}".format(m.name(), m.description()), - 80, initial_indent=self._indent*2, subsequent_indent=self._indent*3)) - print("{}Parameters:".format(self._indent*2)) - for p in m.parameters(): - print(textwrap.fill("* {}".format(p), - 80, initial_indent=self._indent*3, subsequent_indent=self._indent*4)) - print("") - -class ModuleConfig(object): - def __init__(self, config, name, alias): - self._prefix = alias if alias else name - self._config = config - - def prefix(self): - return self._prefix - - def set(self, name, value): - name = self._prefix + name - return self._config.set(name, value) - - def parameter(self, name, default=None): - name = self._prefix + name - return self._config.parameter(name, default) - - def increase(self, name, limit, default): - name = self._prefix + name - return self._config.increase(name, limit, default) +MODULE_HELP = "" class Config(object): - def __init__(self, args): - self._parser = self._parser() - self._store = {} - - if len(args) == 0: - self._parser.print_help() - self._parser.exit() - - self._args = self._parser.parse_args(args) - - for p in self._args.parameters: - key, value = p.split("=") - self.parameter(key, value) - - def set(self, name, value): - self._store[name] = value - - def parameter(self, name, default=None): - if not name in self._store: - self.set(name, default) - return self._store.get(name, default) - - def increase(self, name, limit, default): - if not name in self._store: - self._store[name] = default - return default - - self._store[name] += 1 - if self._store[name] >= limit: - self._store[name] = default - return self._store[name] - - def theme(self): - return self._args.theme + def __init__(self, args = []): + parser = self._create_parser() + self._args = parser.parse_args(args) def modules(self): - result = [] - for m in self._args.modules: - items = m.split(":") - result.append({ "name": items[0], "alias": items[1] if len(items) > 1 else None }) - return result + return map(lambda x: { "name": x, "module": x }, self._args.modules) - def _parser(self): + def _create_parser(self): parser = argparse.ArgumentParser(description="display system data in the i3bar") - parser.add_argument("-m", "--modules", nargs="+", - help="List of modules to load. The order of the list determines " - "their order in the i3bar (from left to right)", - default=[], - ) - parser.add_argument("-l", "--list", - help="List: 'modules', 'themes' ", - choices = [ "modules", "themes" ], - action=print_usage, - ) - parser.add_argument("-p", "--parameters", nargs="+", - help="Provide configuration parameters to individual modules.", - default=[] - ) - parser.add_argument("-t", "--theme", help="Specify which theme to use for " - "drawing the modules", - default="default", - ) - + parser.add_argument("-m", "--modules", nargs="+", default = [], + help = MODULE_HELP) return parser # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/engine.py b/bumblebee/engine.py deleted file mode 100644 index cc0b7b9..0000000 --- a/bumblebee/engine.py +++ /dev/null @@ -1,38 +0,0 @@ -import importlib -import bumblebee.theme -import bumblebee.output -import bumblebee.config -import bumblebee.modules - -class Engine: - def __init__(self, config): - self._modules = [] - self._config = config - self._theme = bumblebee.theme.Theme(config) - self._output = bumblebee.output.output(config) - - def load_module(self, modulespec): - name = modulespec["name"] - module = importlib.import_module("bumblebee.modules.{}".format(name)) - cfg = bumblebee.config.ModuleConfig(self._config, name, modulespec["alias"]) - obj = getattr(module, "Module")(self._output, cfg) - obj.register_callbacks() - return obj - - def load_modules(self): - for m in self._config.modules(): - self._modules.append(self.load_module(m)) - - def run(self): - self._output.start() - - while True: - self._theme.begin() - for m in self._modules: - self._output.draw(m.widgets(), self._theme) - self._output.flush() - self._output.wait() - - self._output.stop() - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/module.py b/bumblebee/module.py deleted file mode 100644 index 0f497a2..0000000 --- a/bumblebee/module.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import pkgutil -import importlib - -import bumblebee.modules - -def modules(): - result = [] - path = os.path.dirname(bumblebee.modules.__file__) - for mod in [ name for _, name, _ in pkgutil.iter_modules([path])]: - result.append(ModuleDescription(mod)) - return result - -class ModuleDescription(object): - def __init__(self, name): - self._name = name - self._mod =importlib.import_module("bumblebee.modules.{}".format(name)) - - def name(self): - return str(self._name) - - def description(self): - return getattr(self._mod, "description", lambda: "n/a")() - - def parameters(self): - return getattr(self._mod, "parameters", lambda: [ "n/a" ])() - -class Module(object): - def __init__(self, output, config): - self._output = output - self._config = config - - def register_callbacks(self): - buttons = [ - { "name": "left-click", "id": 1 }, - { "name": "middle-click", "id": 2 }, - { "name": "right-click", "id": 3 }, - { "name": "wheel-up", "id": 4 }, - { "name": "wheel-down", "id": 5 }, - ] - for button in buttons: - if self._config.parameter(button["name"], None): - output.add_callback( - module=self.instance(), - button=button["id"], - cmd=self._config.parameter(button["name"]) - ) - - def critical(self, widget): - return False - - def warning(self, widget): - return False - - def state(self, widget): - return "default" - - def instance(self, widget=None): - return self._config.prefix() - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/__init__.py b/bumblebee/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py deleted file mode 100644 index 8215693..0000000 --- a/bumblebee/modules/battery.py +++ /dev/null @@ -1,52 +0,0 @@ -import datetime -import bumblebee.module -import os.path - -def description(): - return "Displays battery status, percentage and whether it's charging or discharging." - -def parameters(): - return [ "battery.device: The device to read from (defaults to BAT0)" ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._battery = config.parameter("device", "BAT0") - self._capacity = 100 - self._status = "Unknown" - - def widgets(self): - self._AC = False; - self._path = "/sys/class/power_supply/{}".format(self._battery) - if not os.path.exists(self._path): - self._AC = True; - return bumblebee.output.Widget(self,"AC") - - with open(self._path + "/capacity") as f: - self._capacity = int(f.read()) - self._capacity = self._capacity if self._capacity < 100 else 100 - - return bumblebee.output.Widget(self,"{:02d}%".format(self._capacity)) - - def warning(self, widget): - return self._capacity < self._config.parameter("warning", 20) - - def critical(self, widget): - return self._capacity < self._config.parameter("critical", 10) - - def state(self, widget): - if self._AC: - return "AC" - - with open(self._path + "/status") as f: - self._status = f.read().strip() - - if self._status == "Discharging": - status = "discharging-{}".format(min([ 10, 25, 50, 80, 100] , key=lambda i:abs(i-self._capacity))) - return status - else: - if self._capacity > 95: - return "charged" - return "charging" - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/brightness.py b/bumblebee/modules/brightness.py deleted file mode 100644 index 9227ce8..0000000 --- a/bumblebee/modules/brightness.py +++ /dev/null @@ -1,33 +0,0 @@ -import bumblebee.module - -def description(): - return "Displays brightness percentage" - -def parameters(): - return [ - "brightness.step: Steps (in percent) to increase/decrease brightness on scroll (defaults to 2)", - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._brightness = 0 - self._max = 0 - self._percent = 0 - - step = self._config.parameter("step", 2) - - output.add_callback(module=self.instance(), button=4, cmd="xbacklight +{}%".format(step)) - output.add_callback(module=self.instance(), button=5, cmd="xbacklight -{}%".format(step)) - - def widgets(self): - with open("/sys/class/backlight/intel_backlight/brightness") as f: - self._brightness = int(f.read()) - with open("/sys/class/backlight/intel_backlight/max_brightness") as f: - self._max = int(f.read()) - self._brightness = self._brightness if self._brightness < self._max else self._max - self._percent = int(round(self._brightness * 100 / self._max)) - - return bumblebee.output.Widget(self, "{:02d}%".format(self._percent)) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/caffeine.py b/bumblebee/modules/caffeine.py deleted file mode 100644 index d7c2dad..0000000 --- a/bumblebee/modules/caffeine.py +++ /dev/null @@ -1,38 +0,0 @@ -import subprocess -import shlex - -import bumblebee.module - -def description(): - return "Enable/disable auto screen lock." - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._activated = 0 - output.add_callback(module="caffeine.activate", button=1, cmd=[ 'notify-send "Consuming caffeine"', 'xset s off' ]) - output.add_callback(module="caffeine.deactivate", button=1, cmd=[ 'notify-send "Out of coffee"', 'xset s default' ]) - - def widgets(self): - output = subprocess.check_output(shlex.split("xset q")) - xset_out = output.decode().split("\n") - for line in xset_out: - if line.startswith(" timeout"): - timeout = int(line.split(" ")[4]) - if timeout == 0: - self._activated = 1; - else: - self._activated = 0; - break - - if self._activated == 0: - return bumblebee.output.Widget(self, "", instance="caffeine.activate") - elif self._activated == 1: - return bumblebee.output.Widget(self, "", instance="caffeine.deactivate") - - def state(self, widget): - if self._activated == 1: - return "activated" - else: - return "deactivated" - diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py deleted file mode 100644 index de42ecf..0000000 --- a/bumblebee/modules/cmus.py +++ /dev/null @@ -1,78 +0,0 @@ -import string -import datetime -import subprocess -from collections import defaultdict - -import bumblebee.util -import bumblebee.module - -def description(): - return "Displays the current song and artist playing in cmus" - -def parameters(): - return [ - "cmus.format: Format of the displayed song information, arbitrary tags (as available from cmus-remote -Q) can be used (defaults to {artist} - {title} {position}/{duration})" - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._status = "default" - self._fmt = self._config.parameter("format", "{artist} - {title} {position}/{duration}") - - output.add_callback(module="cmus.prev", button=1, cmd="cmus-remote -r") - output.add_callback(module="cmus.next", button=1, cmd="cmus-remote -n") - output.add_callback(module="cmus.shuffle", button=1, cmd="cmus-remote -S") - output.add_callback(module="cmus.repeat", button=1, cmd="cmus-remote -R") - output.add_callback(module=self.instance(), button=1, cmd="cmus-remote -u") - - def _loadsong(self): - process = subprocess.Popen(["cmus-remote", "-Q"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self._query, self._error = process.communicate() - self._query = self._query.decode("utf-8").split("\n") - self._status = "default" - - def _tags(self): - tags = defaultdict(lambda: '') - self._repeat = False - self._shuffle = False - for line in self._query: - if line.startswith("status"): - status = line.split(" ", 2)[1] - self._status = status - if line.startswith("tag"): - key, value = line.split(" ", 2)[1:] - tags.update({ key: value }) - if line.startswith("duration"): - sec = line.split(" ")[1] - tags.update({ "duration": bumblebee.util.durationfmt(int(sec)) }) - if line.startswith("position"): - sec = line.split(" ")[1] - tags.update({ "position": bumblebee.util.durationfmt(int(sec)) }) - if line.startswith("set repeat "): - self._repeat = False if line.split(" ")[2] == "false" else True - if line.startswith("set shuffle "): - self._shuffle = False if line.split(" ")[2] == "false" else True - - return tags - - def widgets(self): - self._loadsong() - tags = self._tags() - - return [ - bumblebee.output.Widget(self, "", instance="cmus.prev"), - bumblebee.output.Widget(self, string.Formatter().vformat(self._fmt, (), tags)), - bumblebee.output.Widget(self, "", instance="cmus.next"), - bumblebee.output.Widget(self, "", instance="cmus.shuffle"), - bumblebee.output.Widget(self, "", instance="cmus.repeat"), - ] - - def state(self, widget): - if widget.instance() == "cmus.shuffle": - return "on" if self._shuffle else "off" - if widget.instance() == "cmus.repeat": - return "on" if self._repeat else "off" - return self._status - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py deleted file mode 100644 index 619019b..0000000 --- a/bumblebee/modules/cpu.py +++ /dev/null @@ -1,30 +0,0 @@ -import bumblebee.module -import psutil - -def description(): - return "Displays CPU utilization across all CPUs." - -def parameters(): - return [ - "cpu.warning: Warning threshold in % of disk usage (defaults to 70%)", - "cpu.critical: Critical threshold in % of disk usage (defaults to 80%)", - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._perc = psutil.cpu_percent(percpu=False) - - output.add_callback(module=self.instance(), button=1, cmd="gnome-system-monitor") - - def widgets(self): - self._perc = psutil.cpu_percent(percpu=False) - return bumblebee.output.Widget(self, "{:05.02f}%".format(self._perc)) - - def warning(self, widget): - return self._perc > self._config.parameter("warning", 70) - - def critical(self, widget): - return self._perc > self._config.parameter("critical", 80) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/date.py b/bumblebee/modules/date.py deleted file mode 120000 index bde6404..0000000 --- a/bumblebee/modules/date.py +++ /dev/null @@ -1 +0,0 @@ -datetime.py \ No newline at end of file diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py deleted file mode 120000 index 5a370f9..0000000 --- a/bumblebee/modules/datetime.py +++ /dev/null @@ -1 +0,0 @@ -time.py \ No newline at end of file diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py deleted file mode 100644 index 67a79e1..0000000 --- a/bumblebee/modules/disk.py +++ /dev/null @@ -1,40 +0,0 @@ -import os -import bumblebee.util -import bumblebee.module - -def description(): - return "Shows free diskspace, total diskspace and the percentage of free disk space." - -def parameters(): - return [ - "disk.warning: Warning threshold in % (defaults to 80%)", - "disk.critical: Critical threshold in % (defaults to 90%)" - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._path = self._config.parameter("path", "/") - - output.add_callback(module=self.instance(), button=1, cmd="nautilus {}".format(self._path)) - - def widgets(self): - st = os.statvfs(self._path) - - self._size = st.f_frsize*st.f_blocks - self._used = self._size - st.f_frsize*st.f_bavail - self._perc = 100.0*self._used/self._size - - return bumblebee.output.Widget(self, - "{} {}/{} ({:05.02f}%)".format(self._path, - bumblebee.util.bytefmt(self._used), - bumblebee.util.bytefmt(self._size), self._perc) - ) - - def warning(self, widget): - return self._perc > self._config.parameter("warning", 80) - - def critical(self, widget): - return self._perc > self._config.parameter("critical", 90) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/dnf.py b/bumblebee/modules/dnf.py deleted file mode 100644 index 4101a8c..0000000 --- a/bumblebee/modules/dnf.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import absolute_import - -import time -import shlex -import threading -import subprocess - -import bumblebee.module -import bumblebee.util - -def description(): - return "Checks DNF for updated packages and displays the number of /// pending updates." - -def parameters(): - return [ "dnf.interval: Time in seconds between two checks for updates (defaults to 1800)" ] - -def get_dnf_info(obj): - loops = obj.interval() - - for thread in threading.enumerate(): - if thread.name == "MainThread": - main = thread - - while main.is_alive(): - loops += 1 - if loops < obj.interval(): - time.sleep(1) - continue - - loops = 0 - try: - res = subprocess.check_output(shlex.split("dnf updateinfo")) - except Exception as e: - break - - security = 0 - bugfixes = 0 - enhancements = 0 - other = 0 - for line in res.decode().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) - - obj.set("security", security) - obj.set("bugfixes", bugfixes) - obj.set("enhancements", enhancements) - obj.set("other", other) - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - self._counter = {} - self._thread = threading.Thread(target=get_dnf_info, args=(self,)) - self._thread.start() - - def interval(self): - return self._config.parameter("interval", 30*60) - - def set(self, what, value): - self._counter[what] = value - - def get(self, what): - return self._counter.get(what, 0) - - def widgets(self): - result = [] - for t in [ "security", "bugfixes", "enhancements", "other" ]: - result.append(str(self.get(t))) - - return bumblebee.output.Widget(self, "/".join(result)) - - def state(self, widget): - total = sum(self._counter.values()) - if total == 0: return "good" - return "default" - - def warning(self, widget): - total = sum(self._counter.values()) - return total > 0 - - def critical(self, widget): - total = sum(self._counter.values()) - return total > 50 or self._counter.get("security", 0) > 0 - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/layout.py b/bumblebee/modules/layout.py deleted file mode 100644 index b317655..0000000 --- a/bumblebee/modules/layout.py +++ /dev/null @@ -1,67 +0,0 @@ -import subprocess -import shlex -import bumblebee.module -import bumblebee.util - -def description(): - return "Showws current keyboard layout and change it on click." - -def parameters(): - return [ - "layout.lang: pipe-separated list of languages to cycle through (e.g. us|rs|de). Default: en" - ] - - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - self._languages = self._config.parameter("lang", "en").split("|") - self._idx = 0 - - output.add_callback(module=self.instance(), button=1, cmd=self.next_keymap) - output.add_callback(module=self.instance(), button=3, cmd=self.prev_keymap) - - def next_keymap(self, event, widget): - self._idx = self._idx + 1 if self._idx < len(self._languages) - 1 else 0 - self.set_keymap() - - def prev_keymap(self, event, widget): - self._idx = self._idx - 1 if self._idx > 0 else len(self._languages) - 1 - self.set_keymap() - - def set_keymap(self): - tmp = self._languages[self._idx].split(":") - layout = tmp[0] - variant = "" - if len(tmp) > 1: - variant = "-variant {}".format(tmp[1]) - bumblebee.util.execute("setxkbmap -layout {} {}".format(layout, variant)) - - def widgets(self): - res = bumblebee.util.execute("setxkbmap -query") - layout = None - variant = None - for line in res.split("\n"): - if not line: - continue - if "layout" in line: - layout = line.split(":")[1].strip() - if "variant" in line: - variant = line.split(":")[1].strip() - if variant: - layout += ":" + variant - - lang = self._languages[self._idx] - - if lang != layout: - if layout in self._languages: - self._idx = self._languages.index(layout) - else: - self._languages.append(layout) - self._idx = len(self._languages) - 1 - lang = layout - - return bumblebee.output.Widget(self, lang) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/load.py b/bumblebee/modules/load.py deleted file mode 100644 index aad9dfe..0000000 --- a/bumblebee/modules/load.py +++ /dev/null @@ -1,37 +0,0 @@ -import bumblebee.module -import multiprocessing -import os - -def description(): - return "Displays system load." - -def parameters(): - return [ - "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 80% of the number of CPUs)" - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._cpus = 1 - try: - self._cpus = multiprocessing.cpu_count() - except multiprocessing.NotImplementedError as e: - pass - - output.add_callback(module=self.instance(), button=1, cmd="gnome-system-monitor") - - def widgets(self): - self._load = os.getloadavg() - - return bumblebee.output.Widget(self, "{:.02f}/{:.02f}/{:.02f}".format( - self._load[0], self._load[1], self._load[2])) - - def warning(self, widget): - return self._load[0] > self._config.parameter("warning", self._cpus*0.7) - - def critical(self, widget): - return self._load[0] > self._config.parameter("critical", self._cpus*0.8) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/memory.py b/bumblebee/modules/memory.py deleted file mode 100644 index d38ac0f..0000000 --- a/bumblebee/modules/memory.py +++ /dev/null @@ -1,38 +0,0 @@ -import psutil -import bumblebee.module -import bumblebee.util - -def description(): - return "Shows available RAM, total amount of RAM and the percentage of available RAM." - -def parameters(): - return [ - "memory.warning: Warning threshold in % of memory used (defaults to 80%)", - "memory.critical: Critical threshold in % of memory used (defaults to 90%)", - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._mem = psutil.virtual_memory() - - output.add_callback(module=self.instance(), button=1, cmd="gnome-system-monitor") - - def widgets(self): - self._mem = psutil.virtual_memory() - - used = self._mem.total - self._mem.available - - return bumblebee.output.Widget(self, "{}/{} ({:05.02f}%)".format( - bumblebee.util.bytefmt(used), - bumblebee.util.bytefmt(self._mem.total), - self._mem.percent) - ) - - def warning(self, widget): - return self._mem.percent > self._config.parameter("warning", 80) - - def critical(self, widget): - return self._mem.percent > self._config.parameter("critical", 90) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py deleted file mode 100644 index 0db9e3a..0000000 --- a/bumblebee/modules/nic.py +++ /dev/null @@ -1,62 +0,0 @@ -import netifaces -import bumblebee.module - -def description(): - return "Displays the names, IP addresses and status of each available interface." - -def parameters(): - return [ - "nic.exclude: Comma-separated list of interface prefixes to exlude (defaults to: \"lo,virbr,docker,vboxnet,veth\")" - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._exclude = tuple(filter(len, self._config.parameter("exclude", "lo,virbr,docker,vboxnet,veth").split(","))) - - def widgets(self): - result = [] - interfaces = [ i for i in netifaces.interfaces() if not i.startswith(self._exclude) ] - for intf in interfaces: - addr = [] - state = "down" - try: - if netifaces.AF_INET in netifaces.ifaddresses(intf): - for ip in netifaces.ifaddresses(intf)[netifaces.AF_INET]: - if "addr" in ip and ip["addr"] != "": - addr.append(ip["addr"]) - state = "up" - except Exception as e: - addr = [] - widget = bumblebee.output.Widget(self, "{} {} {}".format( - intf, state, ", ".join(addr) - )) - widget.set("intf", intf) - widget.set("state", state) - result.append(widget) - - return result - - 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") - - def state(self, widget): - intf = widget.get("intf") - - iftype = "wireless" if self._iswlan(intf) else "wired" - iftype = "tunnel" if self._istunnel(intf) else iftype - - return "{}-{}".format(iftype, widget.get("state")) - - def warning(self, widget): - return widget.get("state") != "up" - - def critical(self, widget): - return widget.get("state") == "down" - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/pacman.py b/bumblebee/modules/pacman.py deleted file mode 100644 index 3ecb5db..0000000 --- a/bumblebee/modules/pacman.py +++ /dev/null @@ -1,58 +0,0 @@ -import bumblebee.module -import subprocess -import os - -def description(): - return "Displays available updates per repository for pacman." - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - self._count = 0 - - def widgets(self): - path = os.path.dirname(os.path.abspath(__file__)) - if self._count == 0: - self._out = "?/?/?/?" - process = subprocess.Popen([ "{}/../../bin/customupdates".format(path) ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - self._query, self._error = process.communicate() - - if not process.returncode == 0: - self._out = "?/?/?/?" - else: - self._community = 0 - self._core = 0 - self._extra = 0 - self._other = 0 - - for line in self._query.splitlines(): - if line.startswith(b'http'): - if b"community" in line: - self._community += 1 - continue - if b"core" in line: - self._core += 1; - continue - if b"extra" in line: - self._extra += 1 - continue - self._other += 1 - self._out = str(self._core)+"/"+str(self._extra)+"/"+str(self._community)+"/"+str(self._other) - - self._count += 1 - self._count = 0 if self._count > 300 else self._count - return bumblebee.output.Widget(self, "{}".format(self._out)) - - def sumUpdates(self): - return self._core + self._community + self._extra + self._other - - def critical(self, widget): - #return self._sumUpdates(self) - return self.sumUpdates() > 0 - - - - - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/pasink.py b/bumblebee/modules/pasink.py deleted file mode 120000 index 85ea20d..0000000 --- a/bumblebee/modules/pasink.py +++ /dev/null @@ -1 +0,0 @@ -pulseaudio.py \ No newline at end of file diff --git a/bumblebee/modules/pasource.py b/bumblebee/modules/pasource.py deleted file mode 120000 index 85ea20d..0000000 --- a/bumblebee/modules/pasource.py +++ /dev/null @@ -1 +0,0 @@ -pulseaudio.py \ No newline at end of file diff --git a/bumblebee/modules/ping.py b/bumblebee/modules/ping.py deleted file mode 100644 index b523788..0000000 --- a/bumblebee/modules/ping.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import absolute_import - -import re -import time -import shlex -import threading -import subprocess - -import bumblebee.module -import bumblebee.util - -def description(): - return "Periodically checks the RTT of a configurable IP" - -def parameters(): - return [ - "ping.interval: Time in seconds between two RTT checks (defaults to 60)", - "ping.address: IP address to check", - "ping.warning: Threshold for warning state, in seconds (defaults to 1.0)", - "ping.critical: Threshold for critical state, in seconds (defaults to 2.0)", - "ping.timeout: Timeout for waiting for a reply (defaults to 5.0)", - "ping.probes: Number of probes to send (defaults to 5)", - ] - -def get_rtt(obj): - loops = obj.get("interval") - - for thread in threading.enumerate(): - if thread.name == "MainThread": - main = thread - - interval = obj.get("interval") - while main.is_alive(): - loops += 1 - if loops < interval: - time.sleep(1) - continue - - loops = 0 - try: - res = subprocess.check_output(shlex.split("ping -n -q -c {} -W {} {}".format( - obj.get("rtt-probes"), obj.get("rtt-timeout"), obj.get("address") - ))) - obj.set("rtt-unreachable", False) - - for line in res.decode().split("\n"): - if not line.startswith("rtt"): continue - m = re.search(r'([0-9\.]+)/([0-9\.]+)/([0-9\.]+)/([0-9\.]+)\s+(\S+)', line) - - obj.set("rtt-min", float(m.group(1))) - obj.set("rtt-avg", float(m.group(2))) - obj.set("rtt-max", float(m.group(3))) - obj.set("rtt-unit", m.group(5)) - except Exception as e: - obj.set("rtt-unreachable", True) - - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - self._counter = {} - - self.set("address", self._config.parameter("address", "8.8.8.8")) - self.set("interval", self._config.parameter("interval", 60)) - self.set("rtt-probes", self._config.parameter("probes", 5)) - self.set("rtt-timeout", self._config.parameter("timeout", 5.0)) - - self._thread = threading.Thread(target=get_rtt, args=(self,)) - self._thread.start() - - def set(self, what, value): - self._counter[what] = value - - def get(self, what): - return self._counter.get(what, 0) - - def widgets(self): - text = "{}: {:.1f}{}".format( - self.get("address"), - self.get("rtt-avg"), - self.get("rtt-unit") - ) - - if self.get("rtt-unreachable"): - text = "{}: unreachable".format(self.get("address")) - - return bumblebee.output.Widget(self, text) - - def warning(self, widget): - return self.get("rtt-avg") > float(self._config.parameter("warning", 1.0))*1000.0 - - def critical(self, widget): - if self.get("rtt-unreachable"): return True - return self.get("rtt-avg") > float(self._config.parameter("critical", 2.0))*1000.0 - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/pulseaudio.py b/bumblebee/modules/pulseaudio.py deleted file mode 100644 index 079e07c..0000000 --- a/bumblebee/modules/pulseaudio.py +++ /dev/null @@ -1,92 +0,0 @@ -import re -import shlex -import subprocess - -import bumblebee.module -import bumblebee.util - -def description(): - module = __name__.split(".")[-1] - if module == "pasink": - return "Shows volume and mute status of the default PulseAudio Sink." - if module == "pasource": - return "Shows volume and mute status of the default PulseAudio Source." - return "See 'pasource'." - -def parameters(): - return [ "none" ] - - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - self._module = self.__module__.split(".")[-1] - self._left = 0 - self._right = 0 - self._mono = 0 - self._mute = False - channel = "sink" if self._module == "pasink" else "source" - - output.add_callback(module=self.instance(), button=3, - cmd="pavucontrol") - output.add_callback(module=self.instance(), button=1, - cmd="pactl set-{}-mute @DEFAULT_{}@ toggle".format(channel, channel.upper())) - output.add_callback(module=self.instance(), button=4, - cmd="pactl set-{}-volume @DEFAULT_{}@ +2%".format(channel, channel.upper())) - output.add_callback(module=self.instance(), button=5, - cmd="pactl set-{}-volume @DEFAULT_{}@ -2%".format(channel, channel.upper())) - - def widgets(self): - res = subprocess.check_output(shlex.split("pactl info")) - channel = "sinks" if self._module == "pasink" else "sources" - name = None - for line in res.decode().split("\n"): - if line.startswith("Default Sink: ") and channel == "sinks": - name = line[14:] - if line.startswith("Default Source: ") and channel == "sources": - name = line[16:] - - res = subprocess.check_output(shlex.split("pactl list {}".format(channel))) - - found = False - for line in res.decode().split("\n"): - if "Name:" in line and found == True: - break - if name in line: - found = True - if "Mute:" in line and found == True: - self._mute = False if " no" in line.lower() else True - - if "Volume:" in line and found == True: - m = None - if "mono" in line: - m = re.search(r'mono:.*\s*\/\s*(\d+)%', line) - else: - m = re.search(r'left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%', line) - if not m: continue - - if "mono" in line: - self._mono = m.group(1) - else: - self._left = m.group(1) - self._right = m.group(2) - result = "" - if int(self._mono) > 0: - result = "{}%".format(self._mono) - elif self._left == self._right: - result = "{}%".format(self._left) - else: - result="{}%/{}%".format(self._left, self._right) - return bumblebee.output.Widget(self, result) - - def state(self, widget): - return "muted" if self._mute is True else "unmuted" - - def warning(self, widget): - return self._mute - - def critical(self, widget): - return False - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/spacer.py b/bumblebee/modules/spacer.py deleted file mode 100644 index f9b88d3..0000000 --- a/bumblebee/modules/spacer.py +++ /dev/null @@ -1,17 +0,0 @@ -import bumblebee.module -import bumblebee.util - -def description(): - return "Draws a widget with configurable content." - -def parameters(): - return [ "spacer.text: Text to draw (defaults to '')" ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - def widgets(self): - return bumblebee.output.Widget(self, self._config.parameter("text", "")) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/time.py b/bumblebee/modules/time.py deleted file mode 100644 index 68f1beb..0000000 --- a/bumblebee/modules/time.py +++ /dev/null @@ -1,34 +0,0 @@ -from __future__ import absolute_import - -import datetime -import bumblebee.module - -def description(): - return "Displays the current time, using the optional format string as input for strftime." - -def parameters(): - module = __name__.split(".")[-1] - return [ - "{}.format: strftime specification (defaults to {})".format(module, default_format(module)) - ] - -def default_format(module): - default = "%x %X" - if module == "date": - default = "%x" - if module == "time": - default = "%X" - return default - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - module = self.__module__.split(".")[-1] - - self._fmt = self._config.parameter("format", default_format(module)) - - def widgets(self): - return bumblebee.output.Widget(self, datetime.datetime.now().strftime(self._fmt)) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/xrandr.py b/bumblebee/modules/xrandr.py deleted file mode 100644 index 7817143..0000000 --- a/bumblebee/modules/xrandr.py +++ /dev/null @@ -1,89 +0,0 @@ -import bumblebee.module -import bumblebee.util -import re -import os -import sys -import subprocess - -def description(): - return "Shows all connected screens" - -def parameters(): - return [ - ] - -class Module(bumblebee.module.Module): - def __init__(self, output, config): - super(Module, self).__init__(output, config) - - self._widgets = [] - - def toggle(self, event, widget): - path = os.path.dirname(os.path.abspath(__file__)) - toggle_cmd = "{}/../../bin/toggle-display.sh".format(path) - - if widget.get("state") == "on": - bumblebee.util.execute("{} --output {} --off".format(toggle_cmd, widget.get("display"))) - else: - neighbor = None - for w in self._widgets: - if w.get("state") == "on": - neighbor = w - if event.get("button") == 1: - break - - if neighbor == None: - bumblebee.util.execute("{} --output {} --auto".format(toggle_cmd, - widget.get("display"))) - else: - bumblebee.util.execute("{} --output {} --auto --{}-of {}".format(toggle_cmd, - widget.get("display"), "left" if event.get("button") == 1 else "right", - neighbor.get("display"))) - - def widgets(self): - process = subprocess.Popen([ "xrandr", "-q" ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - output, error = process.communicate() - - widgets = [] - - for line in output.split("\n"): - if not " connected" in line: - continue - display = line.split(" ", 2)[0] - m = re.search(r'\d+x\d+\+(\d+)\+\d+', line) - - widget = bumblebee.output.Widget(self, display, instance=display) - widget.set("display", display) - - # not optimal (add callback once per interval), but since - # add_callback() just returns if the callback has already - # been registered, it should be "ok" - self._output.add_callback(module=display, button=1, - cmd=self.toggle) - self._output.add_callback(module=display, button=3, - cmd=self.toggle) - if m: - widget.set("state", "on") - widget.set("pos", int(m.group(1))) - else: - widget.set("state", "off") - widget.set("pos", sys.maxint) - - widgets.append(widget) - - widgets.sort(key=lambda widget : widget.get("pos")) - - self._widgets = widgets - - return widgets - - def state(self, widget): - return widget.get("state", "off") - - def warning(self, widget): - return False - - def critical(self, widget): - return False - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py deleted file mode 100644 index cfdc88b..0000000 --- a/bumblebee/output.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -import inspect -import threading -import bumblebee.util - -def output(args): - import bumblebee.outputs.i3 - return bumblebee.outputs.i3.Output(args) - -class Widget(object): - def __init__(self, obj, text, instance=None): - self._obj = obj - self._text = text - self._store = {} - self._instance = instance - - obj._output.register_widget(self.instance(), self) - - def set(self, key, value): - self._store[key] = value - - def get(self, key, default=None): - return self._store.get(key, default) - - def state(self): - return self._obj.state(self) - - def warning(self): - return self._obj.warning(self) - - def critical(self): - return self._obj.critical(self) - - def module(self): - return self._obj.__module__.split(".")[-1] - - def instance(self): - return self._instance if self._instance else getattr(self._obj, "instance")(self) - - def text(self): - return self._text - -class Command(object): - def __init__(self, command, event, widget): - self._command = command - self._event = event - self._widget = widget - - def __call__(self, *args, **kwargs): - if not isinstance(self._command, list): - self._command = [ self._command ] - - for cmd in self._command: - if not cmd: continue - if inspect.ismethod(cmd): - cmd(self._event, self._widget) - else: - c = cmd.format(*args, **kwargs) - bumblebee.util.execute(c, False) - -class Output(object): - def __init__(self, config): - self._config = config - self._callbacks = {} - self._wait = threading.Condition() - self._wait.acquire() - self._widgets = {} - - def register_widget(self, identity, widget): - self._widgets[identity] = widget - - def redraw(self): - self._wait.acquire() - self._wait.notify() - self._wait.release() - - def add_callback(self, cmd, button, module=None): - if module: - module = module.replace("bumblebee.modules.", "") - - if self._callbacks.get((button, module)): return - - self._callbacks[( - button, - module, - )] = cmd - - def callback(self, event): - cb = self._callbacks.get(( - event.get("button", -1), - None, - ), None) - cb = self._callbacks.get(( - event.get("button", -1), - event.get("instance", event.get("module", None)), - ), cb) - - identity = event.get("instance", event.get("module", None)) - return Command(cb, event, self._widgets.get(identity, None)) - - def wait(self): - self._wait.wait(self._config.parameter("interval", 1)) - - def draw(self, widgets, theme): - if not type(widgets) is list: - widgets = [ widgets ] - self._draw(widgets, theme) - - def start(self): - pass - - def _draw(self, widgets, theme): - pass - - def flush(self): - pass - - def stop(self): - pass - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/outputs/__init__.py b/bumblebee/outputs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/bumblebee/outputs/i3.py b/bumblebee/outputs/i3.py deleted file mode 100644 index d9d1894..0000000 --- a/bumblebee/outputs/i3.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import unicode_literals - -import os -import sys -import json -import shlex -import threading -import subprocess -import bumblebee.output - -def read_input(output): - while True: - line = sys.stdin.readline().strip(",").strip() - if line == "[": continue - if line == "]": break - - event = json.loads(line) - cb = output.callback(event) - if cb: - cb( - name=event.get("name", ""), - instance=event.get("instance", ""), - button=event.get("button", -1) - ) - output.redraw() - -class Output(bumblebee.output.Output): - def __init__(self, args): - super(Output, self).__init__(args) - self._data = [] - - self.add_callback("i3-msg workspace prev_on_output", 4) - self.add_callback("i3-msg workspace next_on_output", 5) - - self._thread = threading.Thread(target=read_input, args=(self,)) - self._thread.start() - - def start(self): - print(json.dumps({ "version": 1, "click_events": True }) + "[") - - def _draw(self, widgets, theme): - for widget in widgets: - if theme.separator(widget): - self._data.append({ - u"full_text": theme.separator(widget), - "color": theme.separator_color(widget), - "background": theme.separator_background(widget), - "separator": False, - "separator_block_width": 0, - }) - - self._data.append({ - u"full_text": " {} {} {}".format( - theme.prefix(widget), - widget.text(), - theme.suffix(widget) - ), - "color": theme.color(widget), - "background": theme.background(widget), - "name": widget.module(), - "instance": widget.instance(), - "separator": theme.default_separators(widget, False), - "separator_block_width": theme.separator_block_width(widget, 0), - }) - theme.next_widget() - - def flush(self): - data = json.dumps(self._data) - self._data = [] - print(data + ",") - sys.stdout.flush() - - def stop(self): - return "]" - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/theme.py b/bumblebee/theme.py deleted file mode 100644 index 42e6c01..0000000 --- a/bumblebee/theme.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import copy -import json -import yaml -import glob - -def getpath(): - return os.path.dirname("{}/../themes/".format(os.path.dirname(os.path.realpath(__file__)))) - -def themes(): - d = getpath() - return [ os.path.basename(f).replace(".json", "") for f in glob.iglob("{}/*.json".format(d)) ] - -class Theme: - def __init__(self, config): - self._config = config - - self._data = self.get_theme(config.theme()) - - for iconset in self._data.get("icons", []): - self.merge(self._data, self.get_theme(iconset)) - - self._defaults = self._data.get("defaults", {}) - self._cycles = self._defaults.get("cycle", []) - self.begin() - - def get_theme(self, name): - for path in [ getpath(), "{}/icons/".format(getpath()) ]: - if os.path.isfile("{}/{}.yaml".format(path, name)): - with open("{}/{}.yaml".format(path, name)) as f: - return yaml.load(f) - if os.path.isfile("{}/{}.json".format(path, name)): - with open("{}/{}.json".format(path, name)) as f: - return json.load(f) - return None - - # algorithm copied from - # http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts - # nicely done :) - def merge(self, target, *args): - 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: - target[key] = copy.deepcopy(value) - return target - - def begin(self): - self._config.set("theme.cycleidx", 0) - self._cycle = self._cycles[0] if len(self._cycles) > 0 else {} - self._background = [ None, None ] - - def next_widget(self): - self._background[1] = self._background[0] - idx = self._config.increase("theme.cycleidx", len(self._cycles), 0) - self._cycle = self._cycles[idx] if len(self._cycles) > idx else {} - - def prefix(self, widget): - return self._get(widget, "prefix", "") - - def suffix(self, widget): - return self._get(widget, "suffix", "") - - def color(self, widget): - result = self._get(widget, "fg") - if widget.warning(): - result = self._get(widget, "fg-warning") - if widget.critical(): - result = self._get(widget, "fg-critical") - return result - - def background(self, widget): - result = self._get(widget, "bg") - if widget.warning(): - result = self._get(widget, "bg-warning") - if widget.critical(): - result = self._get(widget, "bg-critical") - self._background[0] = result - return result - - def separator(self, widget): - return self._get(widget, "separator") - - def default_separators(self, widget, default): - return self._get(widget, "default-separators", default) - - def separator_color(self, widget): - return self.background(widget) - - def separator_background(self, widget): - return self._background[1] - - def separator_block_width(self, widget, default): - return self._get(widget, "separator-block-width", default) - - def _get(self, widget, name, default = None): - module = widget.module() - state = widget.state() - inst = widget.instance() - inst = inst.replace("{}.".format(module), "") - module_theme = self._data.get(module, {}) - state_theme = module_theme.get("states", {}).get(state, {}) - instance_theme = module_theme.get(inst, {}) - instance_state_theme = instance_theme.get("states", {}).get(state, {}) - - value = None - value = self._defaults.get(name, value) - value = self._cycle.get(name, value) - value = module_theme.get(name, value) - value = state_theme.get(name, value) - value = instance_theme.get(name, value) - value = instance_state_theme.get(name, value) - - if type(value) is list: - key = "{}{}".format(widget.instance(), value) - idx = self._config.increase(key, len(value), 0) - value = value[idx] - - return value if value else default - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/util.py b/bumblebee/util.py deleted file mode 100644 index 3a6a60c..0000000 --- a/bumblebee/util.py +++ /dev/null @@ -1,33 +0,0 @@ -import shlex -import subprocess -try: - from exceptions import RuntimeError -except ImportError: - # Python3 doesn't require this anymore - pass - -def bytefmt(num): - for unit in [ "", "Ki", "Mi", "Gi" ]: - if num < 1024.0: - return "{:.2f}{}B".format(num, unit) - num /= 1024.0 - return "{:05.2f%}{}GiB".format(num) - -def durationfmt(duration): - minutes, seconds = divmod(duration, 60) - hours, minutes = divmod(minutes, 60) - res = "{:02d}:{:02d}".format(minutes, seconds) - if hours > 0: res = "{:02d}:{}".format(hours, res) - - return res - -def execute(cmd, wait=True): - args = shlex.split(cmd) - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - - if wait: - out, err = p.communicate() - if p.returncode != 0: - raise RuntimeError("{} exited with {}".format(cmd, p.returncode)) - return out.decode("utf-8") - return None diff --git a/screenshots/battery.png b/screenshots/battery.png deleted file mode 100644 index bdbafee42fd9ce6df799d0cd95888d09fe707a75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1733 zcmV;$20HnPP){x2YX&>x1cG^C)uYGHMs84m;QM9dEwaQWj zI+R5j*<66Ih9!X{k^~aWNNmr1O*S`K?S)Ad5L^eO}2%%1K;#?4r+iZ5Eca({V9Uej7c~z=65`xqK01hYo z^-mF~ue~$yJov7Qw8Z$mg2M3T2VB1#g#9L#}HTsI*~ps;xLru{B=rZg1-3O)97qvc0@` z0teB4DWkcI`|)uJ#O99GhQ~pa5bErbc);a~)lp1K;HR3qc%6N0zgnBQkpNv4(`IOBm{@1 ziF9}F#Q*?uGt{Mf*3fI*H<>o2%CUwbod+DJ-~|LnDR1b005WN+bY#P;`D0qei9EyVL4e?4182~Uf6Yc?*?*UiWCg|vm z$`z{*=gWPcl1Le~EwKTng%En!#q$ks6^&zA#_j=@$r7^eusEJkz%}X&gd2_VZx&7X zcFpQY-pv9b5b_PvGvRJ_4BEni4VhF@;&?`rB}A(yaw07N05;PyGe=smBMLQnr&!(4 z&aW&R!5IF1qs$Q*&HIx~avaE0c)+y^I{Kmj0I^)7?;{h_)ZZHuP;{M{J#n!=TZtg( z=B8v0%W~%3KIv4%igqHwG4G>(a5&Vv7&MCgK%U}KP2%63yz@7<=8DyPjtKz3qA%>q zoH_Sf8jo$BohOQ7o^e?gjBX5!Ez9&~E57P>(t;iFf;&!n@aQL zzto9bC}h(YQiVFN&XTJrHyYzly)hicvRSj>qrFW z>PE8_4*)3HGnJj@M+Q*B7Ec}>{_%QB;Ow-t;JRmos_G=09oZ~K26JdjH$;H?H@~KGaKQj_ zmCLf6I4liN23Bh0NhS4Ky|AK0y1M4#AZoVaKdZ~pw=+|HwkqRa zoGTDl@$P9qp^gJabLjb-TM>w+2=xbdEh!jr(wx%?+j=Ag^4S000ocp%GHl)o>Uw*zE@Zk4iyK=C9+TqA03*q)AGk{_oRB#LhP_ zz}}}II|~>CU{tHKyWm{|(WulHDoXY(Cjr9R8kh1iJ=-fgET-lbO?D@)KY@^!nMNQG zAP7>Pk)M-Smlfs%0B97-@l@Xa;e;U2A~T8wF5I&^yKG!ATVjMrOrXB^`Z<4a!)S99 z=42rVqHL6x7v>AIGfg(<=hwUN9LXfo=~78!<*BX#)ueVV9@UFA6@siZw=d9fcK}7v zQX#*oru@_PP7K3#MXj%}NQ)e}ZE4YBoz)xS`DQ4Jsx|XB`UZXe0FTWq7ZnubWW})O zBp-QKsc^z>2#QyAEQKu*u}HT=og*cOh!_vkWZzMBOPIn_mlR{FMqn@ z4{pS%I?85x`~d&}?DigIvuaDkF1JUkU)hyYEO52T4YSBjzs3A}zIx*q^Yf=2#jwrLR`deFQLV1Q>D&|=g_4!Z8=9I?$f~~T z>fMzyH`DWK<5FbpY-e>=Z$MErc9ij1oyX^Q!fw6Q7S&)efCu~~l`OlJIM`Aj}6L?Uj56951;;zBx&7CVYB5WGD&VqIIuGjO;b2RAow z_Wu#jaf(LTcKAeGZtAse;bw@4i~6L& z))h$zgy;`|N$vd41H*c&?X@O36mjsAXbCNKQnhxzy}N%McJCFcLRR(rs5+I)5$9!p z{LPOig$k_V@$Wdhb14SPq55d&0@jOiSgkRMQ#&Krj?S5X9~E5%AjU^P;yLx%C@=N7LAO z-Ou21PYJ_n-F%Ed#re6yZOVzAm)Rvi~# zX$Bb9qZ*6TU9-fx^JliAetX73%1a+avC|v<8ym|JbA>`8HP=aJ7WHPk^TRjWcx=|t zgP9)Hm?$@!#Ynn8IlbR5{q+@A=8?^^LMH%#D3;blTJTs*Duo=0ilC^@V1hkf1VNY0 zs~J3Q$K8R@R(N&Ig*R{iTQOQ0{vf$b{@Arvd7+6`QM}Nc} hU^zsn(Bb(x`VRs*2Jffwcy<5)002ovPDHLkV1oRXbDIDF diff --git a/screenshots/caffeine.png b/screenshots/caffeine.png deleted file mode 100644 index 54447779afbf96b58344d131d7c390685fd770b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1143 zcmV--1c>{IP)6c$KL}; zVOS9n4u}XiRHV{rp@wQZjWsr|-I8V5VzMpEk|n!Xvb!bQUGI0hCEG66rA?D&YqQkV z8SU&O6q&1E6Qod)4>=q~D2RL&1*gPXn3J|W@cdEWD$^Lx+xf1c-s#yp12`r&c5$F^K?Bd7F~|bhY|ax191jKtT@J%lkn;Jw(Xz&2ojCeDG#c%K=2Rk; z{?}7J2nWDyvj@VFqt62Ya89j)8+cVV-jc)V3k1n#8PuNU*K^6Du|(49bfcj#^{X&z zY9Od|U;LTP{a2(j9ZD%IsG=pS%e#*FDP|ebYfo}spcruZeD95)XBKQv%0n$8;b@<( zMI@}N<&;N5yU`6~Yr;4miNz1X)72_#YnD_=4^BU7b$kBzHhA#+vSHcs(VMTmH8Qa5 z*vRFIR(HA~XC+@Fp zc&VSIlr(iIMO z-G_d4`<}y#y!YxQ2!e9C9I_J{H<+)DPrdWX#fqPC-=wfWr)ncj7=^z;WY=u7KNHDH z1bhI%UIHUJhkJV_jPp;UdqsH&$M4S033>H0v2fpM_xRSFp8l8AWb?zk`g3X(RSx`H zLT*n!pFbo_yR7+x*GF~g_LV2DI97xt7YiMm6(pL?WW zQkhH@Bmh7yn?2C2sbSEmk?zbGcfz|^CR6lFr|ll!!xbypUDLlUj`p1uHq;k?4d{)b z^Jpl%NhpIFrMyEXHIYvR0ElClVZ}mL$DooWjQKQNRrT@ z6zoEG*Oiy`_ve@F?#*}-lfaF`y*(T@`5G2FW>g`{Q<^HC|hD&|A7Ll;A ztpCX4`1aF?3qdDMkMr|ja=?=Imw2i(E}kJQ+p1P{sQQ|s$03vvpfI*002ov JPDHLkV1iwhDMA1M diff --git a/screenshots/cmus.png b/screenshots/cmus.png deleted file mode 100644 index 52c0b5159e2d2eb8dece597c177b523109319651..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5031 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 LNkvXXu0mjfW)Onx diff --git a/screenshots/disk.png b/screenshots/disk.png deleted file mode 100644 index 7a659b8fcdfeb1468d05e49f29f1c98fe3098c53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3721 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 diff --git a/screenshots/dnf.png b/screenshots/dnf.png deleted file mode 100644 index 49924168f31d1a081be9a94ab4063993d73d31ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1113 zcmV-f1g86mP)*HlGt&bp1w4y`s&)?Z*BoZ%eLrdRf6Mp1HxTG@04|M= z%mP7l`s~YB?n&6=U>RYzCI!DZ#pa_#ObuUniDhBICQgYM8z5?EaHZXt5j;Blc!VHu z(G}|MNU_5e0-Ahe^1eV3d1dhQdbcI@uY6NF10!`6nUvryRjZF~N7#8X#tHwjV0hxy z0RRfylG~H*x({$o!#3MTXJvP$&7p|GXvklFd-bTYNEgH*wX-L7 z#u;W|z8$x2B#<50-kIFFdTgm2E&~8OoHsmP)K^mk34J$VZPDgG{b;F0o4OUvk~uK3RV#Lvel0G_WlIC}Q29#{r`7HGUN{FJv;P2=&5BPd`6 zbrzT2{IT=;kWYb>nSimW);ZQyWAv*x0019f_P4d{t372hGMYrDo-})0A-x(8hL6ak zqlT#GKxTk19POlk}MX3SPBG4{iIXIDo@X5T*_VA=wGeq(lYWcGlG0013| z-@SkAcpGN7B>@0f7DiS7R29XNcxFlGIu+q9_1bk$B#zv=+d;6Ve7zc{eczDEMAJ~h)EiPby*PK6Z$dc3H2^=6NvYPy32xzMF%@UB%+SDVO~G_({DzKg07tl z>=thGK5Zmc?->YI_YF@dCQZ-0at~rZe{?${N0r6C6Y&Gv!3JfKb~jxipos^q&Xb!4 zhKWzB<&cUbq0X8o(SJ{IonW fP%Zzt1aaywgMS}|9}1IZ00000NkvXXu0mjfah(r0 diff --git a/screenshots/load.png b/screenshots/load.png deleted file mode 100644 index e136e7e9e1f0e88f5d84e7f4fc2a1b98686f19d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2005 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 diff --git a/screenshots/nic.png b/screenshots/nic.png deleted file mode 100644 index d1512c0772b4b3bc2f09f274af9a215af3b366ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2474 zcmV;b303xqP)X0ssI2ND$sx00003b3#c}2nYz< z;ZNWI000nlMObuGZ)S9NVRB^vQ)qQ`bY*g5g3t*700~A(L_t(|+U;9wP*Z0b{?5t$ zN-pF=Aaad>fFK}(h+suPTM^wZXx(*f-R*Q{-Py6T({*=$bY}l_W@oxH+t$^+^kTPL zt1zn~R76w+xkT;+!X-dRVnRqDAtVqIAZLHXVv2GSLN}#5=b7{4e3$o}=X>&=?|aYt zoj@QV17N_giGjx?z_8hHe1EQI5ylk3fX$;AF!=0_cwB@)fcYqfO$r!>bCMF~796YV zF=;St20#F0ZHp6d*c)2wAqWx+cspXFVZ9RyN#w!o3_gc7F=eW1XdN8a_&ACag?Uk-A>%szmv_@Q1e5iJ(I zeey>l9v1*$G+Sz=op)Q>Yo%TC(Q&iYCg8A1L_$P}AT?gnJ3O}Fbo%~lX^qo0&25kS z2lu5X*LU>T9X{E{h(k}kl3!8RSk=&qAjnS+zSOT&&D!SCt#B~>tK%gj8tvukIu4V5 zcyDHNkKBbI0d`Ps5c4;uUZs*rECx+Jrq&s!H^?xMRw&BTsnnO!lYf8V(|eD*+{cJU z=W@Bc|C*tq6iM_-(QLJ`8T9q}=oTUWY-urxNN~Aa*B>-}UR|fuPR!WmTrStdw0TJN zq_I;*r%<*C`BX9~F($HWXvDF&zS)#QCaudyXRJ1vVqB@yFE|&+b(3L29yf?Npj4w< zWhTXof;pF~>YR&`*qV}ckyW6Zb2~T zQbiRyLln3WWUYwEKZb#{LQ$SdA~_t+bv>>EHY@Af4*KGRl-C};3gyM}Te>OJp5%nW ztTY&gJ(C6DA%gG_!GT?=U)9#X|F3cYfPTv4X|C2AQsX7=c;(#5WAA->b<$*}QOHKq zjMrOdZBO{?wVS#r6OSGA%hF;BnN(5NxT+6{NJy4MUAx=hE(*EqH;M}{T&q-QbSwt_ z)%={)_?U)H832IKWgE;h0D!FR+a%#(<#i2*_hxQzQuKwP@9>XdAgxf8r;?3)*-}~e%yJK~7OEYN{a##dBJ|ctS51j7J=+zZoZm9;Aq8=XRS zIGkYuUP<2W53XE?V3CYKRty`p`gw8+ai68Jf2i;E&2_t2h<^124C zaXMNoa{3ZCLy+GL*Bz?*#W0XoXv(Xe{OR&F_j!+E>`|vIH#G^}M;DyV`Yu_1dJ36H zTr(#X7qN~UzEV4(o-mv{d2IXEC@Pr*0AMlb*(nKUN{Sl0`^;7V@IQLaHB0 z!}wfLFvm1&^(CP8^bZ^i0RUF4UQiK3-vAoh#Tm(U`KWO^Hh0p4wT_S$)?_NKi z8Ygi#O&Cq32XbX2h zqRC<%)#&!7CTR^*zz6RLW<48Z%$Csk1;?~y)?Iu#XBUG;{m1p&tA@v*QQaeNl?_}h zza{M(+_NLm-PAoi3IM2l*y1iGZ;kFBRe2Y2et}7&t`t2fp)>RY{xJ-s6^imO40}4I zFqmWUSY%LOw%QtdWhl?~y7xj=6O&)uzMmQ|Ie)s0&177zx@+;4m{jYH$y=k8Iz0eD zKBnH85Uce_xS;Jl#7WZPB`gL#Enf0U&aV4y-R_kogvZTGPyYAU_aF$6hy)Um;GWk3 z0LS)azfoKmAreqXr0@_yWN65UTI+6V>6Ke-wz7g;K8JlMJ3}nwJ?ifB%0V`wOo@#N zW(5HN(&8m?5n^8$`UkHJ3o_IG_|_>mneW94Mfv?ZQ_hwZ$3=+gR7zUBWKT+BV^^P# z12>p0&3%I}ly2FD`1wtVv~N%>r3Z4^OuErD^JUHb z&OwFmV}D{ab`0UuAet(A#K@jr4+@yKuRB6}n=-t*1uvxMB8UEmq5E)J* zlUMD9`v2V%67XF_4FC7&SE|Nz`i;h;fdjPZ6@@UT`tC7hu{~lqpVE_OC07*qoM6N<$f`yF3^Z)<= diff --git a/screenshots/pacman.png b/screenshots/pacman.png deleted file mode 100644 index 8f4169e3f1b7eeb12ff9a5947b4386ff071d929a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 860 zcmV-i1Ec(jP)q%SB`UCnqYCUSL&8;=pY7SZ=CAy(9LFNyT zLj(CG#|;eR^z4B*5{?pF4{np!v+sRBuif_E_w&8)_gIgfzW{}J|J4l6F%T$Ihzk0n zgM&jf6e{PE14)t=#|odzIp?|L^wALjR@Z7MhL!s$Czr)F_jV}qe_v237IF|aldV(w z$hqm;_~|d+l!%0n?-`owE~A0qyQyz6JW;T0d`Q0&jeVS+WvoR<)!ay}7q>s)^rFRw z0z=T#2g6&z@XJ?kw}RnkVJB3Sn`pYqiw&>r%8@ z9h5b8N5?t>npSyUjZ`e^>}WC0FEQ52qgryYUrA*LIlU-JlBMQSC6$D+H~^quqik!e zpD@kT%A|QUw|lit4|?+J#xyH`Tahc=}E?y8g diff --git a/screenshots/pasink.png b/screenshots/pasink.png deleted file mode 100644 index 2fd6359522b9984ea4e8df7aed4d5465f718163f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1486 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 Hu0mjfCfMGn diff --git a/screenshots/ping.png b/screenshots/ping.png deleted file mode 100644 index 1c8bf2bec8065882dc2306424b8bafe161b1f309..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1655 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.Hwr|tlrd|)1 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 diff --git a/screenshots/time.png b/screenshots/time.png deleted file mode 100644 index 39d5210fde31641aef160d774e958f9220bd2ab3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2456 zcmV;J31{|+P)X0ssI2x3c@Y00003b3#c}2nYz< z;ZNWI000nlMObuGZ)S9NVRB^vQ)qQ`bY*g5g3t*700}fnL_t(&-tAgzP*eF8|KHr3 z7kQBcNWvq8R}vlpqESRaR1k`&v)a}5(OO48wB4@L&TgGf+v&C+cKW4LcRRbS)9JKR zcW~Fb)|L8L7I#HKdC8lA;gtj-2_bKikZ_aS+YeC;5RwAQ_7XELaKLmkAJYHN!FCqN?fYxY?WIqCdP$?^P zdza(Ndf9+sSo_E*jY0+hgpZ#P0ObWz9)}%Q*2@MQfX*?M(Q1nnKQ~BnGkD1nsBl>H zO1VUomP{ohR)WMXmksWw>qpL+l2h2!?k5XKF92EHMRB)O+5Za z@|-zRYje69Z+5KAK~iGu#w(NM1#{WhJ@6rAz@A;GnHfh5G#cDy`0B%Mr#}@zSZ3z1!vEGxMw6OQO-4?mQf` zIo+SNbfbQ3Q(;b2QhTf9I-|9zwQm{qyS>X7Z{F`#s$l{&Z+7sxObCKt0wfU$#6ms* zAc@T|TI?^(p~s6_9IoIcN=x>5(BX;MsHA3%re4)#m@;$a7muA-ULcK3rLJ5?rI5aS zFs#v;p88j|kjKR^?CPC|R)=egEO-3_q$m(|3_bbpH|jP+5GcvZ2o6owMJkz?C*%ho z9Uh0NGg@P`-|M?KxAu(L7TuwfJPuQDdXn3599OAzS?MWJNo|md+6N|9h5-z}-8Hg| z`px!52!gmQnkX%qgus%lQ~*E{o4H_Fd|?hlQ7IR94o^j`8;mx4=a?$|G2vO-%MuX? zQI7KkIcWfZ_JQ&BOQ4*jgeMXQp0m=}AlB1edIf?xwo~ zZyv5`s4RWwM8mzl@fFVqqM=g$ySI<>IIQ)Y@VLyoeQTxiEc@6jT8JeuW&$~~P$VME zpi$=L!}v`A!_9UlkDDkJ33^5~Qjq{AKx87~T=GUW-t#zIK{}5u>>8ZCxl zQSA3XIp*?sA`{{;Y0f3@qRRsSAi^-2gusMn*+p-%006iw`noVe_M*NFKKDTN#y?;A zT4%CRpVmGA0KLiPSw@R;(-Y|l>*JEo??Zil498Y$964OO<>IZ5^-}dH)x~0=R3rcZ z6z64BY>*%@94+}#005}ZA9mGXw)2x%Vv(S^W0=FFWu>KLN1<hnYeOj!2`{ro zXO>Ha={(Ntyyb(&<~6I5Og;cWy*dB_0Q4sN;kvSpp~;7%Y5)K?k-qS>eyiSG*fl(* zU$6lHh9(}j^{T2iNJMhbCu1 zY@k!ARi&~!{ewP#AX5L+=5)0WkL)Nd4x0-HR9d-Ic&vWw?=Rf+uZ|448NBk6Jcn~h zGq=`$gDP#NfZNpC9~_oSGDoN9p2QghfgtFaDlv>iLZWt$etrIGXpP_x1g_ob3yoBM z4D)-Ii7=cm5?r_u{b)5j@p$K^{2rwm$8i7v1VPDcM$pL&W;+pvRT={TU|c)@+D)C+X~E{`P^yBGtU=!+O}|Q;Dd3(x*^rZw z0uvwr0F^?jE-QTNNG$|G7jE45qP{hEJs6sFxR#FAm-9HRhDy0mz+DNs-OA|-xui^% z!)DMF1=%|l`CY@)q4$1kYw>$;9ZF_1LMK=1Z;QjVjQW>R-%8(rNl!SqOTp)|(s}Gt z2dWGfhf1T5s+t@32kBJG_x4qAS@c8(?f9PZA0Da>N@uhXP+~f`oK{rvQPjo$Q!Pg<6q`rX4(H3AcA%Vo#v%guJz zKU%swhbC8_{UIU(Lxfo0bKy9~q*LEMQI7@i!HJpAW9yPEx;*C^n-A{V_>0r~0RSUY zv*#MW2$uC;)HkNq1q)6L!&F*>*}e$=GZf~flZZ&{!E;sBnw-2*;_p>WJnd9J|NVPm zRwj)?$xh|3Uy|jfZz{?b^0-V!g3f50F_><5k2;)7@&4rh2BVr8x7VxI&qI;FpGab{ zY80h$eZ7P*I~-s24ichnifiiSL=uYyp7;JQj#mY7`@PzL;@=Ox(m=>GdRz;yF8&SL W#66KzXm6kZ0000VL_t(&-tAg@P*YbPK6#P= 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 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py deleted file mode 100644 index 3b3fa65..0000000 --- a/tests/modules/test_cpu.py +++ /dev/null @@ -1,26 +0,0 @@ -import unittest - -import bumblebee.config -import bumblebee.modules.cpu - -class FakeOutput(object): - def add_callback(self, cmd, button, module=None): - pass - -class TestCpuModule(unittest.TestCase): - def setUp(self): - output = FakeOutput() - config = bumblebee.config.Config(["-m", "cpu"]) - self.cpu = bumblebee.modules.cpu.Module(output, config, None) - - def test_documentation(self): - self.assertTrue(hasattr(bumblebee.modules.cpu, "description")) - self.assertTrue(hasattr(bumblebee.modules.cpu, "parameters")) - - def test_warning(self): - self.assertTrue(hasattr(self.cpu, "warning")) - - def test_critical(self): - self.assertTrue(hasattr(self.cpu, "critical")) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_config.py b/tests/test_config.py index 6891eab..66fd1d8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,18 @@ import unittest -import bumblebee.config +from bumblebee.config import Config -class TestConfigCreation(unittest.TestCase): +class TestConfig(unittest.TestCase): def setUp(self): - pass + self.defaultConfig = Config() + self.someSimpleModules = [ "foo", "bar", "baz" ] + def test_no_modules_by_default(self): + self.assertEquals(self.defaultConfig.modules(), []) + + def test_load_simple_modules(self): + cfg = Config([ "-m" ] + self.someSimpleModules) + self.assertEquals(cfg.modules(), + map(lambda x: { "name": x, "module": x }, self.someSimpleModules)) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/default.json b/themes/default.json deleted file mode 100644 index ddda5fe..0000000 --- a/themes/default.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "icons": [ "ascii" ], - "defaults": { - "urgent": true, - "fg": "#aabbcc" - } -} diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json deleted file mode 100644 index f04bbc7..0000000 --- a/themes/gruvbox-powerline.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "icons": [ "paxy97", "awesome-fonts" ], - "defaults": { - "prefix": " ", - "suffix" : " ", - "cycle": [ - { - "fg": "#ebdbb2", - "bg": "#1d2021" - }, - { - "fg": "#fbf1c7", - "bg": "#282828" - } - ], - "fg-critical": "#fbf1c7", - "bg-critical": "#cc241d", - "fg-warning": "#1d2021", - "bg-warning": "#d79921", - - "default-separators": false, - "separator-block-width": 0 - }, - "dnf": { - "states": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - } - }, - "battery": { - "states": { - "charged": { - "fg": "#1d2021", - "bg": "#b8bb26" - }, - "AC": { - "fg": "#1d2021", - "bg": "#b8bb26" - } - } - } -} diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json deleted file mode 100644 index bd051ce..0000000 --- a/themes/icons/ascii.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "memory": { - "prefix": "ram" - }, - "cpu": { - "prefix": "cpu" - }, - "disk": { - "prefix": "hdd" - }, - "dnf": { - "prefix": "dnf" - }, - "brightness": { - "prefix": "o" - }, - "cmus": { - "states": { - "playing": { - "prefix": ">" - }, - "paused": { - "prefix": "||" - }, - "stopped": { - "prefix": "[]" - } - }, - "prev": { - "prefix": "|<" - }, - "next": { - "prefix": ">|" - }, - "shuffle": { - "states": { "on": { "prefix": "S" }, "off": { "prefix": "[s]" } } - }, - "repeat": { - "states": { "on": { "prefix": "R" }, "off": { "prefix": "[r]" } } - } - }, - "pasink": { - "states": { - "muted": { - "prefix": "audio(mute)" - }, - "unmuted": { - "prefix": "audio" - } - } - }, - "pasource": { - "states": { - "muted": { - "prefix": "mic(mute)" - }, - "unmuted": { - "prefix": "mic" - } - } - }, - "nic": { - "states": { - "wireless-up": { - "prefix": "wifi" - }, - "wireless-down": { - "prefix": "wifi" - }, - "wired-up": { - "prefix": "lan" - }, - "wired-down": { - "prefix": "lan" - }, - "tunnel-up": { - "prefix": "tun" - }, - "tunnel-down": { - "prefix": "tun" - } - } - }, - "battery": { - "states": { - "charged": { - "suffix": "full" - }, - "charging": { - "suffix": "chr" - }, - "AC": { - "suffix": "ac" - }, - "discharging-10": { - "prefix": "!", - "suffix": "dis" - }, - "discharging-25": { - "suffix": "dis" - }, - "discharging-50": { - "suffix": "dis" - }, - "discharging-80": { - "suffix": "dis" - }, - "discharging-100": { - "suffix": "dis" - } - } - }, - "caffeine": { - "states": { "activated": {"prefix": "caf-on" }, "deactivated": { "prefix": "caf-off " } } - }, - "xrandr": { - "states": { "on": { "prefix": " off "}, "off": { "prefix": " on "} } - } -} diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json deleted file mode 100644 index 62d9294..0000000 --- a/themes/icons/awesome-fonts.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "defaults": { - "separator": "" - }, - "date": { - "prefix": "" - }, - "time": { - "prefix": "" - }, - "memory": { - "prefix": "" - }, - "cpu": { - "prefix": "" - }, - "disk": { - "prefix": "" - }, - "dnf": { - "prefix": "" - }, - "brightness": { - "prefix": "" - }, - "cmus": { - "states": { - "playing": { - "prefix": "" - }, - "paused": { - "prefix": "" - }, - "stopped": { - "prefix": "" - } - }, - "prev": { - "prefix": "" - }, - "next": { - "prefix": "" - }, - "shuffle": { - "states": { "on": { "prefix": "" }, "off": { "prefix": "" } } - }, - "repeat": { - "states": { "on": { "prefix": "" }, "off": { "prefix": "" } } - } - }, - "pasink": { - "states": { - "muted": { - "prefix": "" - }, - "unmuted": { - "prefix": "" - } - } - }, - "pasource": { - "states": { - "muted": { - "prefix": "" - }, - "unmuted": { - "prefix": "" - } - } - }, - "nic": { - "states": { - "wireless-up": { - "prefix": "" - }, - "wireless-down": { - "prefix": "" - }, - "wired-up": { - "prefix": "" - }, - "wired-down": { - "prefix": "" - }, - "tunnel-up": { - "prefix": "" - }, - "tunnel-down": { - "prefix": "" - } - } - }, - "battery": { - "states": { - "charged": { - "prefix": "", - "suffix": "" - }, - "AC": { - "suffix": "" - }, - "charging": { - "prefix": [ "", "", "", "", "" ], - "suffix": "" - }, - "discharging-10": { - "prefix": "", - "suffix": "" - }, - "discharging-25": { - "prefix": "", - "suffix": "" - }, - "discharging-50": { - "prefix": "", - "suffix": "" - }, - "discharging-80": { - "prefix": "", - "suffix": "" - }, - "discharging-100": { - "prefix": "", - "suffix": "" - } - } - }, - "caffeine": { - "states": { "activated": {"prefix": " " }, "deactivated": { "prefix": " " } } - }, - "xrandr": { - "states": { "on": { "prefix": " "}, "off": { "prefix": " "} } - } -} diff --git a/themes/icons/paxy97.json b/themes/icons/paxy97.json deleted file mode 100644 index 9a889ab..0000000 --- a/themes/icons/paxy97.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "memory": { - "prefix": "  " - } -} diff --git a/themes/powerline.json b/themes/powerline.json deleted file mode 100644 index bfec776..0000000 --- a/themes/powerline.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "icons": [ "awesome-fonts" ], - "defaults": { - "cycle": [ - { - "fg": "#ffd700", - "bg": "#d75f00" - }, - { - "fg": "#ffffff", - "bg": "#0087af" - } - ], - "fg-critical": "#ffffff", - "bg-critical": "#ff0000", - "fg-warning": "#d75f00", - "bg-warning": "#ffd700", - - "default_separators": false - }, - "dnf": { - "states": { - "good": { - "fg": "#494949", - "bg": "#41db00" - } - } - }, - "battery": { - "states": { - "charged": { - "fg": "#494949", - "bg": "#41db00" - }, - "AC": { - "fg": "#494949", - "bg": "#41db00" - } - } - } -} diff --git a/themes/solarized-powerline.json b/themes/solarized-powerline.json deleted file mode 100644 index 998b7f4..0000000 --- a/themes/solarized-powerline.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "icons": [ "awesome-fonts" ], - "defaults": { - "cycle": [ - { - "fg": "#93a1a1", - "bg": "#002b36" - }, - { - "fg": "#eee8d5", - "bg": "#586e75" - } - ], - "fg-critical": "#002b36", - "bg-critical": "#dc322f", - "fg-warning": "#002b36", - "bg-warning": "#b58900", - - "default-separators": false, - "separator-block-width": 0 - }, - "dnf": { - "states": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - } - }, - "battery": { - "states": { - "charged": { - "fg": "#002b36", - "bg": "#859900" - }, - "AC": { - "fg": "#002b36", - "bg": "#859900" - } - } - } -} diff --git a/themes/solarized.json b/themes/solarized.json deleted file mode 100644 index 5cc8f86..0000000 --- a/themes/solarized.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "icons": [ "ascii" ], - "defaults": { - "cycle": [ - { - "fg": "#93a1a1", - "bg": "#002b36" - }, - { - "fg": "#eee8d5", - "bg": "#586e75" - } - ], - "fg-critical": "#002b36", - "bg-critical": "#dc322f", - "fg-warning": "#002b36", - "bg-warning": "#b58900", - - "default_separators": false, - "separator": "" - }, - "dnf": { - "states": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - } - }, - "battery": { - "states": { - "charged": { - "fg": "#002b36", - "bg": "#859900" - }, - "AC": { - "fg": "#002b36", - "bg": "#859900" - } - } -} From a720baf407bcca2e3f5967d02a84b6d1a8131693 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 3 Dec 2016 20:45:52 +0100 Subject: [PATCH 006/104] [tests] Add Python3 test run Add testrun to also verify Python3 functionality. + Immediately fix a Python3 incompatibility. see #23 --- bumblebee/config.py | 2 +- runtests.sh | 4 +++- tests/test_config.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bumblebee/config.py b/bumblebee/config.py index d9c9419..d33219c 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -8,7 +8,7 @@ class Config(object): self._args = parser.parse_args(args) def modules(self): - return map(lambda x: { "name": x, "module": x }, self._args.modules) + return list(map(lambda x: { "name": x, "module": x }, self._args.modules)) def _create_parser(self): parser = argparse.ArgumentParser(description="display system data in the i3bar") diff --git a/runtests.sh b/runtests.sh index 58521b5..089f936 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,3 +1,5 @@ #!/bin/sh -nosetests --rednose -v tests/ +test=$(which nosetests) +python2 $test --rednose -v tests/ +python3 $test --rednose -v tests/ diff --git a/tests/test_config.py b/tests/test_config.py index 66fd1d8..d344932 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -13,6 +13,6 @@ class TestConfig(unittest.TestCase): def test_load_simple_modules(self): cfg = Config([ "-m" ] + self.someSimpleModules) self.assertEquals(cfg.modules(), - map(lambda x: { "name": x, "module": x }, self.someSimpleModules)) + list(map(lambda x: { "name": x, "module": x }, self.someSimpleModules))) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 9a5a6d354ac6011af75326d14c5501947dbffd8c Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 3 Dec 2016 20:54:57 +0100 Subject: [PATCH 007/104] [core/config] Add module aliases Allow the user to provide aliases when loading a module multiple times so that the modules can be differentiated (e.g. for passing parameters to a module). see #23 --- bumblebee/config.py | 5 ++++- runtests.sh | 6 ++++++ tests/test_config.py | 14 ++++++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bumblebee/config.py b/bumblebee/config.py index d33219c..17e679d 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -8,7 +8,10 @@ class Config(object): self._args = parser.parse_args(args) def modules(self): - return list(map(lambda x: { "name": x, "module": x }, self._args.modules)) + return list(map(lambda x: { + "module": x.split(":")[0], + "name": x if not ":" in x else x.split(":")[1] + }, self._args.modules)) def _create_parser(self): parser = argparse.ArgumentParser(description="display system data in the i3bar") diff --git a/runtests.sh b/runtests.sh index 089f936..8786182 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,5 +1,11 @@ #!/bin/sh test=$(which nosetests) + +echo "testing $(python2 -V 2>&1)" python2 $test --rednose -v tests/ + +echo + +echo "testing $(python3 -V 2>&1)" python3 $test --rednose -v tests/ diff --git a/tests/test_config.py b/tests/test_config.py index d344932..aa860f6 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,13 +6,23 @@ class TestConfig(unittest.TestCase): def setUp(self): self.defaultConfig = Config() self.someSimpleModules = [ "foo", "bar", "baz" ] + self.someAliasModules = [ "foo:a", "bar:b", "baz:c" ] + def test_no_modules_by_default(self): self.assertEquals(self.defaultConfig.modules(), []) def test_load_simple_modules(self): cfg = Config([ "-m" ] + self.someSimpleModules) - self.assertEquals(cfg.modules(), - list(map(lambda x: { "name": x, "module": x }, self.someSimpleModules))) + self.assertEquals(cfg.modules(), list(map(lambda x: { + "name": x, "module": x + }, self.someSimpleModules))) + + def test_load_alias_modules(self): + cfg = Config([ "-m" ] + self.someAliasModules) + self.assertEquals(cfg.modules(), list(map(lambda x: { + "module": x.split(":")[0], + "name": x.split(":")[1] + }, self.someAliasModules))) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From a7eff64294b94cbb029c49cc72eabd0f04fa04a4 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 08:02:40 +0100 Subject: [PATCH 008/104] [lint] Add tool to run pylint on all files pylint all *.py files and fix the errors reported so far. --- bumblebee/config.py | 37 +++++++++++++++++++++++++------------ runlint.sh | 3 +++ tests/test_config.py | 19 ++++++++++--------- 3 files changed, 38 insertions(+), 21 deletions(-) create mode 100755 runlint.sh diff --git a/bumblebee/config.py b/bumblebee/config.py index 17e679d..58e3ca2 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -1,22 +1,35 @@ +"""Configuration handling + +This module provides configuration information (loaded modules, +module parameters, etc.) to all other components +""" + import argparse MODULE_HELP = "" +def create_parser(): + """Create the argument parser""" + parser = argparse.ArgumentParser(description="display system data in the i3bar") + parser.add_argument("-m", "--modules", nargs="+", default=[], + help=MODULE_HELP) + return parser + class Config(object): - def __init__(self, args = []): - parser = self._create_parser() - self._args = parser.parse_args(args) + """Top-level configuration class + + Parses commandline arguments and provides non-module + specific configuration information. + """ + def __init__(self, args=None): + parser = create_parser() + self._args = parser.parse_args(args if args else []) def modules(self): - return list(map(lambda x: { + """Return a list of all activated modules""" + return [{ "module": x.split(":")[0], - "name": x if not ":" in x else x.split(":")[1] - }, self._args.modules)) - - def _create_parser(self): - parser = argparse.ArgumentParser(description="display system data in the i3bar") - parser.add_argument("-m", "--modules", nargs="+", default = [], - help = MODULE_HELP) - return parser + "name": x if not ":" in x else x.split(":")[1], + } for x in self._args.modules] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/runlint.sh b/runlint.sh new file mode 100755 index 0000000..0d35fb5 --- /dev/null +++ b/runlint.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +find . -name "*.py"|xargs pylint diff --git a/tests/test_config.py b/tests/test_config.py index aa860f6..c87b51a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,3 +1,4 @@ +# pylint: disable=C0103,C0111 import unittest from bumblebee.config import Config @@ -5,24 +6,24 @@ from bumblebee.config import Config class TestConfig(unittest.TestCase): def setUp(self): self.defaultConfig = Config() - self.someSimpleModules = [ "foo", "bar", "baz" ] - self.someAliasModules = [ "foo:a", "bar:b", "baz:c" ] + self.someSimpleModules = ["foo", "bar", "baz"] + self.someAliasModules = ["foo:a", "bar:b", "baz:c"] def test_no_modules_by_default(self): self.assertEquals(self.defaultConfig.modules(), []) def test_load_simple_modules(self): - cfg = Config([ "-m" ] + self.someSimpleModules) - self.assertEquals(cfg.modules(), list(map(lambda x: { + cfg = Config(["-m"] + self.someSimpleModules) + self.assertEquals(cfg.modules(), [{ "name": x, "module": x - }, self.someSimpleModules))) + } for x in self.someSimpleModules]) def test_load_alias_modules(self): - cfg = Config([ "-m" ] + self.someAliasModules) - self.assertEquals(cfg.modules(), list(map(lambda x: { + cfg = Config(["-m"] + self.someAliasModules) + self.assertEquals(cfg.modules(), [{ "module": x.split(":")[0], - "name": x.split(":")[1] - }, self.someAliasModules))) + "name": x.split(":")[1], + } for x in self.someAliasModules]) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From e5201187a29a2189481b80db76395e1e613920f9 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 08:25:11 +0100 Subject: [PATCH 009/104] [tests/config] Small refactoring (renaming) --- tests/test_config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index c87b51a..90e9816 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,17 +9,16 @@ class TestConfig(unittest.TestCase): self.someSimpleModules = ["foo", "bar", "baz"] self.someAliasModules = ["foo:a", "bar:b", "baz:c"] - def test_no_modules_by_default(self): self.assertEquals(self.defaultConfig.modules(), []) - def test_load_simple_modules(self): + def test_simple_modules(self): cfg = Config(["-m"] + self.someSimpleModules) self.assertEquals(cfg.modules(), [{ "name": x, "module": x } for x in self.someSimpleModules]) - def test_load_alias_modules(self): + def test_alias_modules(self): cfg = Config(["-m"] + self.someAliasModules) self.assertEquals(cfg.modules(), [{ "module": x.split(":")[0], From cf1693548b0fe595319074a1549b984ce36f8742 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 08:37:01 +0100 Subject: [PATCH 010/104] [engine] Add initial version of event loop engine Re-add the engine that is responsible for tying together input, output, etc. see #23 --- bumblebee-status | 18 ++++++++++++++++++ bumblebee/engine.py | 25 +++++++++++++++++++++++++ tests/test_engine.py | 13 +++++++++++++ 3 files changed, 56 insertions(+) mode change 100644 => 100755 bumblebee-status create mode 100644 bumblebee/engine.py create mode 100644 tests/test_engine.py diff --git a/bumblebee-status b/bumblebee-status old mode 100644 new mode 100755 index e69de29..b048a61 --- a/bumblebee-status +++ b/bumblebee-status @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import sys +import bumblebee.engine +import bumblebee.config + +def main(): + config = bumblebee.config.Config(sys.argv[1:]) + engine = bumblebee.engine.Engine( + config=config + ) + + engine.run() + +if __name__ == "__main__": + main() + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/engine.py b/bumblebee/engine.py new file mode 100644 index 0000000..f9996b6 --- /dev/null +++ b/bumblebee/engine.py @@ -0,0 +1,25 @@ +"""Core application engine""" + +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): + self._running = True + + def running(self): + """Check whether the event loop is running""" + return self._running + + def stop(self): + """Stop the event loop""" + self._running = False + + def run(self): + """Start the event loop""" + while self.running(): + pass + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..0d81578 --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,13 @@ +# pylint: disable=C0103,C0111 +import unittest + +from bumblebee.engine import Engine + +class TestEngine(unittest.TestCase): + def setUp(self): + self.engine = Engine(None) + + def test_stop(self): + self.assertTrue(self.engine.running()) + self.engine.stop() + self.assertFalse(self.engine.running()) From a2c6214baa9d65dc5432f335541e66e0f2b84508 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 11:09:10 +0100 Subject: [PATCH 011/104] [core/engine] Add module loading logic Allow the engine to load modules from the bumblebee/modules/ directory. see #23 --- bumblebee/engine.py | 26 ++++++++++++++++++++++++++ bumblebee/modules/__init__.py | 0 bumblebee/modules/test.py | 11 +++++++++++ runtests.sh | 10 ++++++---- tests/test_engine.py | 23 ++++++++++++++++++++++- 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 bumblebee/modules/__init__.py create mode 100644 bumblebee/modules/test.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index f9996b6..0264083 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -1,5 +1,18 @@ """Core application engine""" +import importlib + +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): + pass + class Engine(object): """Engine for driving the application @@ -8,6 +21,19 @@ class Engine(object): """ def __init__(self, config): self._running = True + self._modules = [] + self.load_modules(config.modules()) + + def load_modules(self, modules): + """Load specified modules and return them as list""" + for module in modules: + self._modules.append(self.load_module(module["module"])) + return self._modules + + def load_module(self, module_name): + """Load specified module and return it as object""" + module = importlib.import_module("bumblebee.modules.{}".format(module_name)) + return getattr(module, "Module")(self) def running(self): """Check whether the event loop is running""" diff --git a/bumblebee/modules/__init__.py b/bumblebee/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py new file mode 100644 index 0000000..028f742 --- /dev/null +++ b/bumblebee/modules/test.py @@ -0,0 +1,11 @@ +# pylint: disable=C0111,R0903 + +"""Test module""" + +import bumblebee.engine + +class Module(bumblebee.engine.Module): + def __init__(self, engine): + super(Module, self).__init__(engine) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/runtests.sh b/runtests.sh index 8786182..cbc5a2a 100755 --- a/runtests.sh +++ b/runtests.sh @@ -2,10 +2,12 @@ test=$(which nosetests) -echo "testing $(python2 -V 2>&1)" +echo "testing with $(python2 -V 2>&1)" python2 $test --rednose -v tests/ -echo +if [ $? == 0 ]; then + echo -echo "testing $(python3 -V 2>&1)" -python3 $test --rednose -v tests/ + echo "testing with $(python3 -V 2>&1)" + python3 $test --rednose -v tests/ +fi diff --git a/tests/test_engine.py b/tests/test_engine.py index 0d81578..7a6152f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -2,12 +2,33 @@ import unittest from bumblebee.engine import Engine +from bumblebee.config import Config class TestEngine(unittest.TestCase): def setUp(self): - self.engine = Engine(None) + self.engine = Engine(Config()) + self.testModule = "test" + self.testModuleSpec = "bumblebee.modules.{}".format(self.testModule) + self.testModules = [ + { "module": "test", "name": "a" }, + { "module": "test", "name": "b" }, + ] def test_stop(self): self.assertTrue(self.engine.running()) self.engine.stop() self.assertFalse(self.engine.running()) + + def test_load_module(self): + module = self.engine.load_module(self.testModule) + self.assertEquals(module.__module__, self.testModuleSpec) + + def test_load_modules(self): + modules = self.engine.load_modules(self.testModules) + self.assertEquals(len(modules), len(self.testModules)) + self.assertEquals( + [ module.__module__ for module in modules ], + [ self.testModuleSpec for module in modules ] + ) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 6f52825ef0ef21c5d52af9a4a88e35bcb43645a4 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 12:26:20 +0100 Subject: [PATCH 012/104] [core/output] Add initial version of i3bar output Add output handler for i3bar protocol and add some tests for it. Right now, it only support start and end. see #23 --- bumblebee/engine.py | 6 +++++- bumblebee/output.py | 21 +++++++++++++++++++++ runlint.sh | 2 +- tests/test_engine.py | 8 ++++---- tests/test_i3baroutput.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 bumblebee/output.py create mode 100644 tests/test_i3baroutput.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 0264083..0213062 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -19,7 +19,8 @@ class Engine(object): This class connects input/output, instantiates all required modules and drives the "event loop" """ - def __init__(self, config): + def __init__(self, config, output=None): + self._output = output self._running = True self._modules = [] self.load_modules(config.modules()) @@ -45,7 +46,10 @@ class Engine(object): def run(self): """Start the event loop""" + self._output.start() while self.running(): pass + self._output.stop() + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py new file mode 100644 index 0000000..d09f63a --- /dev/null +++ b/bumblebee/output.py @@ -0,0 +1,21 @@ +# pylint: disable=R0201 + +"""Output classes""" + +import sys +import json + +class I3BarOutput(object): + """Manage output according to the i3bar protocol""" + def __init__(self): + pass + + def start(self): + """Print start preamble for i3bar protocol""" + sys.stdout.write(json.dumps({"version": 1, "click_events": True}) + "[\n") + + def stop(self): + """Finish i3bar protocol""" + sys.stdout.write("]\n") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/runlint.sh b/runlint.sh index 0d35fb5..022534d 100755 --- a/runlint.sh +++ b/runlint.sh @@ -1,3 +1,3 @@ #!/bin/sh -find . -name "*.py"|xargs pylint +find . -name "*.py"|xargs pylint --disable=R0903 diff --git a/tests/test_engine.py b/tests/test_engine.py index 7a6152f..cc5c69f 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -10,8 +10,8 @@ class TestEngine(unittest.TestCase): self.testModule = "test" self.testModuleSpec = "bumblebee.modules.{}".format(self.testModule) self.testModules = [ - { "module": "test", "name": "a" }, - { "module": "test", "name": "b" }, + {"module": "test", "name": "a"}, + {"module": "test", "name": "b"}, ] def test_stop(self): @@ -27,8 +27,8 @@ class TestEngine(unittest.TestCase): modules = self.engine.load_modules(self.testModules) self.assertEquals(len(modules), len(self.testModules)) self.assertEquals( - [ module.__module__ for module in modules ], - [ self.testModuleSpec for module in modules ] + [module.__module__ for module in modules], + [self.testModuleSpec for module in modules] ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py new file mode 100644 index 0000000..9fff324 --- /dev/null +++ b/tests/test_i3baroutput.py @@ -0,0 +1,28 @@ +# pylint: disable=C0103,C0111 +import json +import unittest +import mock +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from bumblebee.output import I3BarOutput + +class TestI3BarOutput(unittest.TestCase): + def setUp(self): + self.output = I3BarOutput() + self.expectedStart = json.dumps({"version": 1, "click_events": True}) + "[\n" + self.expectedStop = "]\n" + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_start(self, stdout): + self.output.start() + self.assertEquals(self.expectedStart, stdout.getvalue()) + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_stop(self, stdout): + self.output.stop() + self.assertEquals(self.expectedStop, stdout.getvalue()) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 712d958e18b9f228b83231e050c130153157ad58 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 12:53:18 +0100 Subject: [PATCH 013/104] [core/output] Add widget drawing Add basic drawing of widgets. Each module instance returns a list of widgets using the widgets() method which is then forwarded to the draw() method of the configured output. see #23 --- bumblebee/engine.py | 5 ++++- bumblebee/output.py | 11 +++++++++++ tests/__init__.py | 0 tests/test_i3baroutput.py | 16 ++++++++++++++++ tests/util.py | 10 ++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/util.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 0213062..746556b 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -48,7 +48,10 @@ class Engine(object): """Start the event loop""" self._output.start() while self.running(): - pass + widgets = [] + for module in self._modules: + widgets += module.widgets() + self._output.draw(widgets) self._output.stop() diff --git a/bumblebee/output.py b/bumblebee/output.py index d09f63a..3c08671 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -18,4 +18,15 @@ class I3BarOutput(object): """Finish i3bar protocol""" sys.stdout.write("]\n") + def draw(self, widgets): + """Draw a number of widgets""" + if not isinstance(widgets, list): + widgets = [widgets] + result = [] + for widget in widgets: + result.append({ + u"full_text": widget.text() + }) + sys.stdout.write(json.dumps(result)) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 9fff324..0ab88b8 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -1,4 +1,5 @@ # pylint: disable=C0103,C0111 + import json import unittest import mock @@ -8,12 +9,14 @@ except ImportError: from io import StringIO from bumblebee.output import I3BarOutput +from tests.util import MockWidget class TestI3BarOutput(unittest.TestCase): def setUp(self): self.output = I3BarOutput() self.expectedStart = json.dumps({"version": 1, "click_events": True}) + "[\n" self.expectedStop = "]\n" + self.someWidget = MockWidget("foo bar baz") @mock.patch("sys.stdout", new_callable=StringIO) def test_start(self, stdout): @@ -25,4 +28,17 @@ class TestI3BarOutput(unittest.TestCase): self.output.stop() self.assertEquals(self.expectedStop, stdout.getvalue()) + @mock.patch("sys.stdout", new_callable=StringIO) + def test_draw_single_widget(self, stdout): + self.output.draw(self.someWidget) + result = json.loads(stdout.getvalue())[0] + self.assertEquals(result["full_text"], self.someWidget.text()) + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_draw_multiple_widgets(self, stdout): + self.output.draw([self.someWidget, self.someWidget]) + result = json.loads(stdout.getvalue()) + for res in result: + self.assertEquals(res["full_text"], self.someWidget.text()) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..202da49 --- /dev/null +++ b/tests/util.py @@ -0,0 +1,10 @@ +# pylint: disable=C0103,C0111 + +class MockWidget(object): + def __init__(self, text): + self._text = text + + def text(self): + return self._text + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From b6eb3ee8e675d0f3a77d20ccfca4db29aeb0398e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 16:14:43 +0100 Subject: [PATCH 014/104] [output/i3bar] Add flush method flush() terminates a single iteration of widget drawing. see #23 --- bumblebee-status | 5 ++++- bumblebee/engine.py | 1 + bumblebee/modules/test.py | 3 +++ bumblebee/output.py | 5 +++++ runlint.sh | 2 +- tests/test_i3baroutput.py | 6 ++++++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index b048a61..4d1cfe6 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -3,11 +3,14 @@ import sys import bumblebee.engine import bumblebee.config +import bumblebee.output def main(): config = bumblebee.config.Config(sys.argv[1:]) + output = bumblebee.output.I3BarOutput() engine = bumblebee.engine.Engine( - config=config + config=config, + output=output, ) engine.run() diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 746556b..ddf6396 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -52,6 +52,7 @@ class Engine(object): for module in self._modules: widgets += module.widgets() self._output.draw(widgets) + self._output.flush() self._output.stop() diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index 028f742..79c0ec9 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -8,4 +8,7 @@ class Module(bumblebee.engine.Module): def __init__(self, engine): super(Module, self).__init__(engine) + def widgets(self): + return [] + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 3c08671..96ebe0b 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -29,4 +29,9 @@ class I3BarOutput(object): }) sys.stdout.write(json.dumps(result)) + def flush(self): + """Flushes output""" + sys.stdout.write(",\n") + sys.stdout.flush() + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/runlint.sh b/runlint.sh index 022534d..0555628 100755 --- a/runlint.sh +++ b/runlint.sh @@ -1,3 +1,3 @@ #!/bin/sh -find . -name "*.py"|xargs pylint --disable=R0903 +find . -name "*.py"|xargs pylint --disable=R0903,R0201 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 0ab88b8..c8e01e0 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -41,4 +41,10 @@ class TestI3BarOutput(unittest.TestCase): for res in result: self.assertEquals(res["full_text"], self.someWidget.text()) + @mock.patch("sys.stdout", new_callable=StringIO) + def test_flush(self, stdout): + self.output.flush() + self.assertEquals(",\n", stdout.getvalue()) + + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 8855f1155be9bfbc52f35a11520a9f8164ede29b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 16:23:44 +0100 Subject: [PATCH 015/104] [core/errors] Add custom exceptions Add custom exceptions and add error handling to the engine's module loading logic. I.e. when a non-existent module is loaded, an exception is thrown now. see #23 --- bumblebee-status | 6 +++++- bumblebee/engine.py | 8 +++++++- bumblebee/error.py | 11 +++++++++++ tests/test_engine.py | 10 ++++++++++ 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 bumblebee/error.py diff --git a/bumblebee-status b/bumblebee-status index 4d1cfe6..fcb5679 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -16,6 +16,10 @@ def main(): engine.run() if __name__ == "__main__": - main() + try: + main() + except bumblebee.error.BaseError as error: + sys.stderr.write("fatal: {}\n".format(error)) + sys.exit(1) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/engine.py b/bumblebee/engine.py index ddf6396..3c07d98 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -1,6 +1,8 @@ """Core application engine""" +import time import importlib +import bumblebee.error class Module(object): """Module instance base class @@ -33,7 +35,10 @@ class Engine(object): def load_module(self, module_name): """Load specified module and return it as object""" - module = importlib.import_module("bumblebee.modules.{}".format(module_name)) + try: + module = importlib.import_module("bumblebee.modules.{}".format(module_name)) + except ImportError as error: + raise bumblebee.error.ModuleLoadError(error) return getattr(module, "Module")(self) def running(self): @@ -53,6 +58,7 @@ class Engine(object): widgets += module.widgets() self._output.draw(widgets) self._output.flush() + time.sleep(1) self._output.stop() diff --git a/bumblebee/error.py b/bumblebee/error.py new file mode 100644 index 0000000..b07cc24 --- /dev/null +++ b/bumblebee/error.py @@ -0,0 +1,11 @@ +"""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 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_engine.py b/tests/test_engine.py index cc5c69f..2cb5ea3 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,6 +1,7 @@ # pylint: disable=C0103,C0111 import unittest +from bumblebee.error import ModuleLoadError from bumblebee.engine import Engine from bumblebee.config import Config @@ -8,6 +9,7 @@ class TestEngine(unittest.TestCase): def setUp(self): self.engine = Engine(Config()) self.testModule = "test" + self.invalidModule = "no-such-module" self.testModuleSpec = "bumblebee.modules.{}".format(self.testModule) self.testModules = [ {"module": "test", "name": "a"}, @@ -23,6 +25,14 @@ class TestEngine(unittest.TestCase): module = self.engine.load_module(self.testModule) self.assertEquals(module.__module__, self.testModuleSpec) + def test_load_invalid_module(self): + with self.assertRaises(ModuleLoadError): + self.engine.load_module(self.invalidModule) + + def test_load_none(self): + with self.assertRaises(ModuleLoadError): + self.engine.load_module(None) + def test_load_modules(self): modules = self.engine.load_modules(self.testModules) self.assertEquals(len(modules), len(self.testModules)) From aacc56a4e27e3cba239beff5370d449157c7129c Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 17:45:42 +0100 Subject: [PATCH 016/104] [modules/cpu] Add initial version of CPU utilization module Re-enable the CPU utilization module as proof-of-concept for the new core engine. see #23 --- bumblebee/engine.py | 8 +++++--- bumblebee/modules/cpu.py | 18 ++++++++++++++++++ bumblebee/modules/test.py | 2 +- bumblebee/output.py | 13 +++++++++++-- tests/modules/__init__.py | 0 tests/modules/test_cpu.py | 16 ++++++++++++++++ tests/test_engine.py | 13 ++++++++++++- tests/test_i3baroutput.py | 4 ++-- tests/util.py | 21 ++++++++++++++++++++- 9 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 bumblebee/modules/cpu.py create mode 100644 tests/modules/__init__.py create mode 100644 tests/modules/test_cpu.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 3c07d98..e2b4826 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -55,10 +55,12 @@ class Engine(object): while self.running(): widgets = [] for module in self._modules: - widgets += module.widgets() - self._output.draw(widgets) + module_widgets = module.widgets() + widgets += module_widgets if isinstance(module_widgets, list) else [module_widgets] + self._output.draw(widgets=widgets, engine=self) self._output.flush() - time.sleep(1) + if self.running(): + time.sleep(1) self._output.stop() diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py new file mode 100644 index 0000000..ecc2376 --- /dev/null +++ b/bumblebee/modules/cpu.py @@ -0,0 +1,18 @@ +# pylint: disable=C0111,R0903 + +"""Displays CPU utilization across all CPUs.""" + +import psutil +import bumblebee.engine + +class Module(bumblebee.engine.Module): + def __init__(self, engine): + super(Module, self).__init__(engine) + self._utilization = psutil.cpu_percent(percpu=False) + + def widgets(self): + self._utilization = psutil.cpu_percent(percpu=False) + + return bumblebee.output.Widget(full_text="{:05.02f}%".format(self._utilization)) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index 79c0ec9..94b99c4 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -9,6 +9,6 @@ class Module(bumblebee.engine.Module): super(Module, self).__init__(engine) def widgets(self): - return [] + return bumblebee.output.Widget(full_text="test") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 96ebe0b..bd05450 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -5,6 +5,15 @@ import sys import json +class Widget(object): + """Represents a single visible block in the status bar""" + def __init__(self, full_text): + self._full_text = full_text + + def full_text(self): + """Retrieve the full text to display in the widget""" + return self._full_text + class I3BarOutput(object): """Manage output according to the i3bar protocol""" def __init__(self): @@ -18,14 +27,14 @@ class I3BarOutput(object): """Finish i3bar protocol""" sys.stdout.write("]\n") - def draw(self, widgets): + def draw(self, widgets, engine=None): """Draw a number of widgets""" if not isinstance(widgets, list): widgets = [widgets] result = [] for widget in widgets: result.append({ - u"full_text": widget.text() + u"full_text": widget.full_text() }) sys.stdout.write(json.dumps(result)) diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py new file mode 100644 index 0000000..9c3be94 --- /dev/null +++ b/tests/modules/test_cpu.py @@ -0,0 +1,16 @@ +# pylint: disable=C0103,C0111 + +import unittest + +from bumblebee.modules.cpu import Module +from tests.util import assertWidgetAttributes + +class TestCPUModule(unittest.TestCase): + def setUp(self): + self.module = Module(None) + + def test_widgets(self): + widget = self.module.widgets() + assertWidgetAttributes(self, widget) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_engine.py b/tests/test_engine.py index 2cb5ea3..03f8334 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,13 +1,17 @@ # pylint: disable=C0103,C0111 + import unittest from bumblebee.error import ModuleLoadError from bumblebee.engine import Engine from bumblebee.config import Config +from tests.util import MockOutput + class TestEngine(unittest.TestCase): def setUp(self): - self.engine = Engine(Config()) + self.engine = Engine(config=Config(), output=MockOutput()) + self.singleWidgetModule = [{"module": "test"}] self.testModule = "test" self.invalidModule = "no-such-module" self.testModuleSpec = "bumblebee.modules.{}".format(self.testModule) @@ -41,4 +45,11 @@ class TestEngine(unittest.TestCase): [self.testModuleSpec for module in modules] ) + def test_run(self): + self.engine.load_modules(self.singleWidgetModule) + try: + self.engine.run() + except Exception as e: + self.fail(e) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index c8e01e0..4a277ae 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -32,14 +32,14 @@ class TestI3BarOutput(unittest.TestCase): def test_draw_single_widget(self, stdout): self.output.draw(self.someWidget) result = json.loads(stdout.getvalue())[0] - self.assertEquals(result["full_text"], self.someWidget.text()) + self.assertEquals(result["full_text"], self.someWidget.full_text()) @mock.patch("sys.stdout", new_callable=StringIO) def test_draw_multiple_widgets(self, stdout): self.output.draw([self.someWidget, self.someWidget]) result = json.loads(stdout.getvalue()) for res in result: - self.assertEquals(res["full_text"], self.someWidget.text()) + self.assertEquals(res["full_text"], self.someWidget.full_text()) @mock.patch("sys.stdout", new_callable=StringIO) def test_flush(self, stdout): diff --git a/tests/util.py b/tests/util.py index 202da49..542b220 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,10 +1,29 @@ # pylint: disable=C0103,C0111 +from bumblebee.output import Widget + +def assertWidgetAttributes(test, widget): + test.assertTrue(isinstance(widget, Widget)) + test.assertTrue(hasattr(widget, "full_text")) + +class MockOutput(object): + def start(self): + pass + + def stop(self): + pass + + def draw(self, widgets, engine): + engine.stop() + + def flush(self): + pass + class MockWidget(object): def __init__(self, text): self._text = text - def text(self): + def full_text(self): return self._text # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 60ae92c8e3789e33318693e792c614ba0bb30aed Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 4 Dec 2016 18:10:04 +0100 Subject: [PATCH 017/104] [core/themes] Prepare adding of themeing support * Add framework JSON definition for themes * Add framework test module * Add framework module see #23 --- bumblebee/theme.py | 6 ++++ testjson.sh | 3 ++ tests/test_engine.py | 2 +- tests/test_theme.py | 3 ++ tests/util.py | 2 +- themes/icons/awesome-fonts.json | 54 +++++++++++++++++++++++++++++++++ themes/solarized-powerline.json | 35 +++++++++++++++++++++ 7 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 bumblebee/theme.py create mode 100755 testjson.sh create mode 100644 tests/test_theme.py create mode 100644 themes/icons/awesome-fonts.json create mode 100644 themes/solarized-powerline.json diff --git a/bumblebee/theme.py b/bumblebee/theme.py new file mode 100644 index 0000000..e83ea10 --- /dev/null +++ b/bumblebee/theme.py @@ -0,0 +1,6 @@ +"""Theme support""" + +class Theme(object): + pass + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/testjson.sh b/testjson.sh new file mode 100755 index 0000000..ce473ea --- /dev/null +++ b/testjson.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +find themes/ -name "*.json"|xargs cat|json_verify -s diff --git a/tests/test_engine.py b/tests/test_engine.py index 03f8334..113e7ce 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,4 +1,4 @@ -# pylint: disable=C0103,C0111 +# pylint: disable=C0103,C0111,W0703 import unittest diff --git a/tests/test_theme.py b/tests/test_theme.py new file mode 100644 index 0000000..65014f9 --- /dev/null +++ b/tests/test_theme.py @@ -0,0 +1,3 @@ +# pylint: disable=C0103,C0111 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 542b220..00001d5 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,4 +1,4 @@ -# pylint: disable=C0103,C0111 +# pylint: disable=C0103,C0111,W0613 from bumblebee.output import Widget diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json new file mode 100644 index 0000000..34abf24 --- /dev/null +++ b/themes/icons/awesome-fonts.json @@ -0,0 +1,54 @@ +{ + "defaults": { "separator": "" }, + "date": { "prefix": "" }, + "time": { "prefix": "" }, + "memory": { "prefix": "" }, + "cpu": { "prefix": "" }, + "disk": { "prefix": "" }, + "dnf": { "prefix": "" }, + "brightness": { "prefix": "" }, + "cmus": { + "playing": { "prefix": "" }, + "paused": { "prefix": "" }, + "stopped": { "prefix": "" }, + "prev": { "prefix": "" }, + "next": { "prefix": "" }, + "shuffle": { "on": { "prefix": "" }, "off": { "prefix": "" } }, + "repeat": { "on": { "prefix": "" }, "off": { "prefix": "" } } + }, + "pasink": { + "muted": { "prefix": "" }, + "unmuted": { "prefix": "" } + }, + "pasource": { + "muted": { "prefix": "" }, + "unmuted": { "prefix": "" } + }, + "nic": { + "wireless-up": { "prefix": "" }, + "wireless-down": { "prefix": "" }, + "wired-up": { "prefix": "" }, + "wired-down": { "prefix": "" }, + "tunnel-up": { "prefix": "" }, + "tunnel-down": { "prefix": "" } + }, + "battery": { + "charged": { "prefix": "", "suffix": "" }, + "AC": { "suffix": "" }, + "charging": { + "prefix": [ "", "", "", "", "" ], + "suffix": "" + }, + "discharging-10": { "prefix": "", "suffix": "" }, + "discharging-25": { "prefix": "", "suffix": "" }, + "discharging-50": { "prefix": "", "suffix": "" }, + "discharging-80": { "prefix": "", "suffix": "" }, + "discharging-100": { "prefix": "", "suffix": "" } + }, + "caffeine": { + "activated": {"prefix": " " }, "deactivated": { "prefix": " " } + }, + "xrandr": { + "on": { "prefix": " "}, "off": { "prefix": " "} + } +} diff --git a/themes/solarized-powerline.json b/themes/solarized-powerline.json new file mode 100644 index 0000000..1326777 --- /dev/null +++ b/themes/solarized-powerline.json @@ -0,0 +1,35 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "default-separators": false, + "separator-block-width": 0, + "cycle": [ + { "fg": "#93a1a1", "bg": "#002b36" }, + { "fg": "#eee8d5", "bg": "#586e75" } + ], + "warning": { + "fg": "#002b36", + "bg": "#b58900" + }, + "critical": { + "fg": "#002b36", + "bg": "#dc322f" + } + }, + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#002b36", + "bg": "#859900" + }, + "AC": { + "fg": "#002b36", + "bg": "#859900" + } + } +} From f64520357927026ba061a38737af224aa14fd965 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 08:44:54 +0100 Subject: [PATCH 018/104] [core] Widget creation/update overhaul Until now, widgets were re-created during each iteration. For multiple, reasons, using static widget objects is much easier, so instead of creating new widgets continuously, modules now create the widgets during instantiation and get the list of widgets passed as parameter whenever an update occurs. During the update, they can still manipulate the widget list by removing and adding elements as needed. Advantages: * Less memory fragmentation (fewer (de)allocations) * Easier event management (widgets now have static IDs) * Easier module code (widget contents can simply be the result of a callback) see #23 --- bumblebee/engine.py | 16 ++++++++++------ bumblebee/modules/cpu.py | 11 +++++++---- bumblebee/modules/test.py | 8 +++++--- bumblebee/output.py | 5 ++++- bumblebee/theme.py | 1 + runlint.sh | 2 +- tests/modules/test_cpu.py | 11 +++++++++-- tests/test_module.py | 25 +++++++++++++++++++++++++ tests/util.py | 3 +++ 9 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 tests/test_module.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index e2b4826..2ba5107 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -12,8 +12,14 @@ class Module(object): (e.g. CPU utilization, disk usage, etc.) derive from this base class. """ - def __init__(self, engine): - pass + def __init__(self, engine, widgets): + self._widgets = [] + if widgets: + self._widgets = widgets if isinstance(widgets, list) else [widgets] + + def widgets(self): + """Return the widgets to draw for this module""" + return self._widgets class Engine(object): """Engine for driving the application @@ -53,11 +59,9 @@ class Engine(object): """Start the event loop""" self._output.start() while self.running(): - widgets = [] for module in self._modules: - module_widgets = module.widgets() - widgets += module_widgets if isinstance(module_widgets, list) else [module_widgets] - self._output.draw(widgets=widgets, engine=self) + module.update(module.widgets()) + self._output.draw(widgets=module.widgets(), engine=self) self._output.flush() if self.running(): time.sleep(1) diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index ecc2376..dbee32e 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -7,12 +7,15 @@ import bumblebee.engine class Module(bumblebee.engine.Module): def __init__(self, engine): - super(Module, self).__init__(engine) + super(Module, self).__init__(engine, + bumblebee.output.Widget(full_text=self.utilization) + ) self._utilization = psutil.cpu_percent(percpu=False) - def widgets(self): - self._utilization = psutil.cpu_percent(percpu=False) + def utilization(self): + return "{:05.02f}%".format(self._utilization) - return bumblebee.output.Widget(full_text="{:05.02f}%".format(self._utilization)) + def update(self, widgets): + self._utilization = psutil.cpu_percent(percpu=False) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index 94b99c4..61af7e9 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -6,9 +6,11 @@ import bumblebee.engine class Module(bumblebee.engine.Module): def __init__(self, engine): - super(Module, self).__init__(engine) + super(Module, self).__init__(engine, + bumblebee.output.Widget(full_text="test") + ) - def widgets(self): - return bumblebee.output.Widget(full_text="test") + def update(self, widgets): + pass # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index bd05450..727d634 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -12,7 +12,10 @@ class Widget(object): def full_text(self): """Retrieve the full text to display in the widget""" - return self._full_text + if callable(self._full_text): + return self._full_text() + else: + return self._full_text class I3BarOutput(object): """Manage output according to the i3bar protocol""" diff --git a/bumblebee/theme.py b/bumblebee/theme.py index e83ea10..e65bc41 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -1,6 +1,7 @@ """Theme support""" class Theme(object): + """Represents a collection of icons and colors""" pass # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/runlint.sh b/runlint.sh index 0555628..6902ce9 100755 --- a/runlint.sh +++ b/runlint.sh @@ -1,3 +1,3 @@ #!/bin/sh -find . -name "*.py"|xargs pylint --disable=R0903,R0201 +find . -name "*.py"|xargs pylint --disable=R0903,R0201,C0330 diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 9c3be94..9ec7ecb 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -10,7 +10,14 @@ class TestCPUModule(unittest.TestCase): self.module = Module(None) def test_widgets(self): - widget = self.module.widgets() - assertWidgetAttributes(self, widget) + widgets = self.module.widgets() + for widget in widgets: + assertWidgetAttributes(self, widget) + + def test_update(self): + widgets = self.module.widgets() + self.module.update(widgets) + self.test_widgets() + self.assertEquals(widgets, self.module.widgets()) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 0000000..34aa69d --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,25 @@ +# pylint: disable=C0103,C0111,W0703 + +import unittest + +from bumblebee.engine import Module +from tests.util import MockWidget + +class TestModule(unittest.TestCase): + def setUp(self): + self.widget = MockWidget("foo") + self.moduleWithoutWidgets = Module(engine=None, widgets=None) + self.moduleWithOneWidget = Module(engine=None, widgets=self.widget) + self.moduleWithMultipleWidgets = Module(engine=None, + widgets=[self.widget, self.widget, self.widget] + ) + + def test_empty_widgets(self): + self.assertEquals(self.moduleWithoutWidgets.widgets(), []) + + def test_single_widget(self): + self.assertEquals(self.moduleWithOneWidget.widgets(), [self.widget]) + + def test_multiple_widgets(self): + for widget in self.moduleWithMultipleWidgets.widgets(): + self.assertEquals(widget, self.widget) diff --git a/tests/util.py b/tests/util.py index 00001d5..e3bef15 100644 --- a/tests/util.py +++ b/tests/util.py @@ -23,6 +23,9 @@ class MockWidget(object): def __init__(self, text): self._text = text + def update(self, widgets): + pass + def full_text(self): return self._text From c44744fa50f18a69b1c4e73dea53ee292bcd407b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 09:04:47 +0100 Subject: [PATCH 019/104] [core/output] Small refactoring --- bumblebee/output.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index 727d634..47289ce 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -30,15 +30,19 @@ class I3BarOutput(object): """Finish i3bar protocol""" sys.stdout.write("]\n") + def draw_widget(self, result, widget): + """Draw a single widget""" + result.append({ + u"full_text": widget.full_text() + }) + def draw(self, widgets, engine=None): """Draw a number of widgets""" if not isinstance(widgets, list): widgets = [widgets] result = [] for widget in widgets: - result.append({ - u"full_text": widget.full_text() - }) + self.draw_widget(result, widget) sys.stdout.write(json.dumps(result)) def flush(self): From 2399cf9af1ad54d90381a5a40b2d5172c5dc6522 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 09:44:05 +0100 Subject: [PATCH 020/104] [core/themes] Add theme loading Load a theme from a JSON file. --- bumblebee/error.py | 4 ++++ bumblebee/theme.py | 25 ++++++++++++++++++++++++- tests/test_theme.py | 24 +++++++++++++++++++++++- themes/invalid.json | 1 + 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 themes/invalid.json diff --git a/bumblebee/error.py b/bumblebee/error.py index b07cc24..129f02d 100644 --- a/bumblebee/error.py +++ b/bumblebee/error.py @@ -8,4 +8,8 @@ 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 diff --git a/bumblebee/theme.py b/bumblebee/theme.py index e65bc41..e07c1b8 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -1,7 +1,30 @@ """Theme support""" +import os +import json + +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__)))) + class Theme(object): """Represents a collection of icons and colors""" - pass + def __init__(self, name): + self._theme = self.load(name) + + def load(self, name): + """Load and parse a theme file""" + path = theme_path() + themefile = "{}/{}.json".format(path, name) + if os.path.isfile(themefile): + try: + with open(themefile) as data: + return json.load(data) + except ValueError as exception: + raise bumblebee.error.ThemeLoadError("JSON error: {}".format(exception)) + else: + raise bumblebee.error.ThemeLoadError("no such theme: {}".format(name)) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_theme.py b/tests/test_theme.py index 65014f9..746f95b 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -1,3 +1,25 @@ -# pylint: disable=C0103,C0111 +# pylint: disable=C0103,C0111,W0703 + +import unittest +from bumblebee.theme import Theme +from bumblebee.error import ThemeLoadError + +class TestTheme(unittest.TestCase): + def setUp(self): + pass + + def test_load_valid_theme(self): + try: + Theme("solarized-powerline") + except Exception as e: + self.fail(e) + + def test_load_nonexistent_theme(self): + with self.assertRaises(ThemeLoadError): + Theme("no-such-theme") + + def test_load_invalid_theme(self): + with self.assertRaises(ThemeLoadError): + Theme("invalid") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/invalid.json b/themes/invalid.json new file mode 100644 index 0000000..d510f27 --- /dev/null +++ b/themes/invalid.json @@ -0,0 +1 @@ +this is really not json From e6666becb3f519a4ed42d088466e57a778b185cf Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 10:17:25 +0100 Subject: [PATCH 021/104] [tests/theme] Refactor --- tests/test_theme.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_theme.py b/tests/test_theme.py index 746f95b..23ad13c 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -6,7 +6,8 @@ from bumblebee.error import ThemeLoadError class TestTheme(unittest.TestCase): def setUp(self): - pass + self.nonexistentThemeName = "no-such-theme" + self.invalidThemeName = "invalid" def test_load_valid_theme(self): try: @@ -16,10 +17,10 @@ class TestTheme(unittest.TestCase): def test_load_nonexistent_theme(self): with self.assertRaises(ThemeLoadError): - Theme("no-such-theme") + Theme(self.nonexistentThemeName) def test_load_invalid_theme(self): with self.assertRaises(ThemeLoadError): - Theme("invalid") + Theme(self.invalidThemeName) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 64f5fc100e6fa3383f70bc2516938b4c95b9ceea Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 11:31:20 +0100 Subject: [PATCH 022/104] [core/theme] Add prefix/postfix methods Add a way to specify prefix and postfix strings to the full text of a widget's text. Currently, the theme does not fill those yet. see #23 --- bumblebee-status | 4 +++- bumblebee/config.py | 8 +++++++- bumblebee/output.py | 11 ++++++++--- bumblebee/theme.py | 8 ++++++++ tests/test_i3baroutput.py | 31 ++++++++++++++++++++++++++++++- tests/util.py | 17 +++++++++++++++++ 6 files changed, 73 insertions(+), 6 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index fcb5679..28e8e88 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -1,13 +1,15 @@ #!/usr/bin/env python import sys +import bumblebee.theme import bumblebee.engine import bumblebee.config import bumblebee.output def main(): config = bumblebee.config.Config(sys.argv[1:]) - output = bumblebee.output.I3BarOutput() + theme = bumblebee.theme.Theme(config.theme()) + output = bumblebee.output.I3BarOutput(theme=theme) engine = bumblebee.engine.Engine( config=config, output=output, diff --git a/bumblebee/config.py b/bumblebee/config.py index 58e3ca2..386f12f 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -7,12 +7,14 @@ module parameters, etc.) to all other components import argparse MODULE_HELP = "" +THEME_HELP = "" def create_parser(): """Create the argument parser""" parser = argparse.ArgumentParser(description="display system data in the i3bar") parser.add_argument("-m", "--modules", nargs="+", default=[], - help=MODULE_HELP) + help=MODULE_HELP) + parser.add_argument("-t", "--theme", default="default", help=THEME_HELP) return parser class Config(object): @@ -32,4 +34,8 @@ class Config(object): "name": x if not ":" in x else x.split(":")[1], } for x in self._args.modules] + def theme(self): + """Return the name of the selected theme""" + return self._args.theme + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 47289ce..f614447 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -19,8 +19,8 @@ class Widget(object): class I3BarOutput(object): """Manage output according to the i3bar protocol""" - def __init__(self): - pass + def __init__(self, theme): + self._theme = theme def start(self): """Print start preamble for i3bar protocol""" @@ -32,8 +32,13 @@ class I3BarOutput(object): def draw_widget(self, result, widget): """Draw a single widget""" + full_text = widget.full_text() + if self._theme.prefix(): + full_text = "{}{}".format(self._theme.prefix(), full_text) + if self._theme.suffix(): + full_text = "{}{}".format(full_text, self._theme.suffix()) result.append({ - u"full_text": widget.full_text() + u"full_text": "{}".format(full_text) }) def draw(self, widgets, engine=None): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index e07c1b8..f056c85 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -14,6 +14,14 @@ class Theme(object): def __init__(self, name): self._theme = self.load(name) + def prefix(self): + """Return the theme prefix for a widget's full text""" + return None + + def suffix(self): + """Return the theme suffix for a widget's full text""" + return None + def load(self, name): """Load and parse a theme file""" path = theme_path() diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 4a277ae..067cf42 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -10,10 +10,12 @@ except ImportError: from bumblebee.output import I3BarOutput from tests.util import MockWidget +from tests.util import MockTheme class TestI3BarOutput(unittest.TestCase): def setUp(self): - self.output = I3BarOutput() + self.theme = MockTheme() + self.output = I3BarOutput(self.theme) self.expectedStart = json.dumps({"version": 1, "click_events": True}) + "[\n" self.expectedStop = "]\n" self.someWidget = MockWidget("foo bar baz") @@ -46,5 +48,32 @@ class TestI3BarOutput(unittest.TestCase): self.output.flush() self.assertEquals(",\n", stdout.getvalue()) + @mock.patch("sys.stdout", new_callable=StringIO) + def test_prefix(self, stdout): + self.theme.set_prefix(" - ") + self.output.draw(self.someWidget) + result = json.loads(stdout.getvalue())[0] + self.assertEquals(result["full_text"], "{}{}".format( + self.theme.prefix(), self.someWidget.full_text()) + ) + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_suffix(self, stdout): + self.theme.set_suffix(" - ") + self.output.draw(self.someWidget) + result = json.loads(stdout.getvalue())[0] + self.assertEquals(result["full_text"], "{}{}".format( + self.someWidget.full_text(), self.theme.suffix()) + ) + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_bothfix(self, stdout): + self.theme.set_suffix(" - ") + self.theme.set_prefix(" * ") + self.output.draw(self.someWidget) + result = json.loads(stdout.getvalue())[0] + self.assertEquals(result["full_text"], "{}{}{}".format( + self.theme.prefix(), self.someWidget.full_text(), self.theme.suffix()) + ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index e3bef15..aa67cdc 100644 --- a/tests/util.py +++ b/tests/util.py @@ -29,4 +29,21 @@ class MockWidget(object): def full_text(self): return self._text +class MockTheme(object): + def __init__(self): + self._prefix = None + self._suffix = None + + def set_prefix(self, value): + self._prefix = value + + def set_suffix(self, value): + self._suffix = value + + def prefix(self): + return self._prefix + + def suffix(self): + return self._suffix + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 394ef61760425c2a6323a98aa1f35bce6d298ba5 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 11:52:47 +0100 Subject: [PATCH 023/104] [core/theme] Add support for default -> prefix/suffix in themes Themes can now define default prefix and suffix strings. see #23 --- bumblebee/output.py | 10 ++++++---- bumblebee/theme.py | 25 ++++++++++++++++++++----- tests/test_i3baroutput.py | 6 +++--- tests/test_theme.py | 15 ++++++++++++++- tests/util.py | 4 ++-- 5 files changed, 45 insertions(+), 15 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index f614447..4b9603b 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -33,10 +33,12 @@ class I3BarOutput(object): def draw_widget(self, result, widget): """Draw a single widget""" full_text = widget.full_text() - if self._theme.prefix(): - full_text = "{}{}".format(self._theme.prefix(), full_text) - if self._theme.suffix(): - full_text = "{}{}".format(full_text, self._theme.suffix()) + prefix = self._theme.prefix(widget) + suffix = self._theme.suffix(widget) + if prefix: + full_text = "{}{}".format(prefix, full_text) + if suffix: + full_text = "{}{}".format(full_text, suffix) result.append({ u"full_text": "{}".format(full_text) }) diff --git a/bumblebee/theme.py b/bumblebee/theme.py index f056c85..b9462f2 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -12,15 +12,24 @@ def theme_path(): class Theme(object): """Represents a collection of icons and colors""" def __init__(self, name): - self._theme = self.load(name) + theme = self.load(name) + self._init(self.load(name)) - def prefix(self): + def _init(self, data): + """Initialize theme from data structure""" + self._defaults = data.get("defaults", {}) + + def prefix(self, widget): """Return the theme prefix for a widget's full text""" - return None + return self._get(widget, "prefix", None) - def suffix(self): + def suffix(self, widget): """Return the theme suffix for a widget's full text""" - return None + return self._get(widget, "suffix", None) + + def loads(self, data): + theme = json.loads(data) + self._init(theme) def load(self, name): """Load and parse a theme file""" @@ -35,4 +44,10 @@ class Theme(object): else: raise bumblebee.error.ThemeLoadError("no such theme: {}".format(name)) + def _get(self, widget, name,default=None): + value = default + value = self._defaults.get(name, value) + + return value + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 067cf42..e621611 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -54,7 +54,7 @@ class TestI3BarOutput(unittest.TestCase): self.output.draw(self.someWidget) result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( - self.theme.prefix(), self.someWidget.full_text()) + self.theme.prefix(self.someWidget), self.someWidget.full_text()) ) @mock.patch("sys.stdout", new_callable=StringIO) @@ -63,7 +63,7 @@ class TestI3BarOutput(unittest.TestCase): self.output.draw(self.someWidget) result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( - self.someWidget.full_text(), self.theme.suffix()) + self.someWidget.full_text(), self.theme.suffix(self.someWidget)) ) @mock.patch("sys.stdout", new_callable=StringIO) @@ -73,7 +73,7 @@ class TestI3BarOutput(unittest.TestCase): self.output.draw(self.someWidget) result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}{}".format( - self.theme.prefix(), self.someWidget.full_text(), self.theme.suffix()) + self.theme.prefix(self.someWidget), self.someWidget.full_text(), self.theme.suffix(self.someWidget)) ) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_theme.py b/tests/test_theme.py index 23ad13c..6d57a71 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -3,15 +3,18 @@ import unittest from bumblebee.theme import Theme from bumblebee.error import ThemeLoadError +from tests.util import MockWidget class TestTheme(unittest.TestCase): def setUp(self): self.nonexistentThemeName = "no-such-theme" self.invalidThemeName = "invalid" + self.validThemeName = "solarized-powerline" + self.someWidget = MockWidget("foo") def test_load_valid_theme(self): try: - Theme("solarized-powerline") + Theme(self.validThemeName) except Exception as e: self.fail(e) @@ -23,4 +26,14 @@ class TestTheme(unittest.TestCase): with self.assertRaises(ThemeLoadError): Theme(self.invalidThemeName) + def test_prefix(self): + theme = Theme(self.validThemeName) + theme.loads('{"defaults": { "prefix": "test" }}') + self.assertEquals(theme.prefix(self.someWidget), "test") + + def test_suffix(self): + theme = Theme(self.validThemeName) + theme.loads('{"defaults": { "suffix": "test" }}') + self.assertEquals(theme.suffix(self.someWidget), "test") + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index aa67cdc..462c6c8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -40,10 +40,10 @@ class MockTheme(object): def set_suffix(self, value): self._suffix = value - def prefix(self): + def prefix(self, widget): return self._prefix - def suffix(self): + def suffix(self, widget): return self._suffix # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 562fd85ca2a59d21909d9e1829a397de39154af7 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 12:09:21 +0100 Subject: [PATCH 024/104] [core/theme] Add support for icon themes Allow sub-themes ("iconsets") to be merged into the "main" theme. That way, effectively, it's possible to define colors and icons in separate JSON files. see #23 --- bumblebee/theme.py | 41 +++++++++++++++++++++++++++++++++++------ tests/test_theme.py | 25 ++++++++++++++++--------- tests/util.py | 7 +++++++ themes/icons/test.json | 6 ++++++ themes/test.json | 7 +++++++ 5 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 themes/icons/test.json create mode 100644 themes/test.json diff --git a/bumblebee/theme.py b/bumblebee/theme.py index b9462f2..3b53f7d 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -1,6 +1,7 @@ """Theme support""" import os +import copy import json import bumblebee.error @@ -12,11 +13,13 @@ def theme_path(): class Theme(object): """Represents a collection of icons and colors""" def __init__(self, name): - theme = self.load(name) self._init(self.load(name)) def _init(self, data): """Initialize theme from data structure""" + for iconset in data.get("icons", []): + self._merge(data, self._load_icons(iconset)) + self._theme = data self._defaults = data.get("defaults", {}) def prefix(self, widget): @@ -31,10 +34,14 @@ class Theme(object): theme = json.loads(data) self._init(theme) - def load(self, name): + def _load_icons(self, name): + path = "{}/icons/".format(theme_path()) + return self.load(name, path=path) + + def load(self, name, path=theme_path()): """Load and parse a theme file""" - path = theme_path() themefile = "{}/{}.json".format(path, name) + if os.path.isfile(themefile): try: with open(themefile) as data: @@ -44,10 +51,32 @@ class Theme(object): else: raise bumblebee.error.ThemeLoadError("no such theme: {}".format(name)) - def _get(self, widget, name,default=None): - value = default - value = self._defaults.get(name, value) + def _get(self, widget, name, default=None): + + module_theme = self._theme.get(widget.module(), {}) + + value = self._defaults.get(name, default) + value = module_theme.get(name, value) return value + # algorithm copied from + # http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts + # nicely done :) + def _merge(self, target, *args): + 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: + target[key] = copy.deepcopy(value) + return target + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_theme.py b/tests/test_theme.py index 6d57a71..a956d4f 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -9,8 +9,15 @@ class TestTheme(unittest.TestCase): def setUp(self): self.nonexistentThemeName = "no-such-theme" self.invalidThemeName = "invalid" - self.validThemeName = "solarized-powerline" + self.validThemeName = "test" self.someWidget = MockWidget("foo") + self.theme = Theme(self.validThemeName) + + self.widgetTheme = "test-widget" + self.defaultPrefix = "default-prefix" + self.defaultSuffix = "default-suffix" + self.widgetPrefix = "widget-prefix" + self.widgetSuffix = "widget-suffix" def test_load_valid_theme(self): try: @@ -26,14 +33,14 @@ class TestTheme(unittest.TestCase): with self.assertRaises(ThemeLoadError): Theme(self.invalidThemeName) - def test_prefix(self): - theme = Theme(self.validThemeName) - theme.loads('{"defaults": { "prefix": "test" }}') - self.assertEquals(theme.prefix(self.someWidget), "test") + def test_default_prefix(self): + self.assertEquals(self.theme.prefix(self.someWidget), self.defaultPrefix) - def test_suffix(self): - theme = Theme(self.validThemeName) - theme.loads('{"defaults": { "suffix": "test" }}') - self.assertEquals(theme.suffix(self.someWidget), "test") + def test_default_suffix(self): + self.assertEquals(self.theme.suffix(self.someWidget), self.defaultSuffix) + + def test_widget_prefix(self): + self.someWidget.set_module(self.widgetTheme) + self.assertEquals(self.theme.prefix(self.someWidget), self.widgetPrefix) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 462c6c8..28a562d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -22,10 +22,17 @@ class MockOutput(object): class MockWidget(object): def __init__(self, text): self._text = text + self._module = None + + def set_module(self, name): + self._module = name def update(self, widgets): pass + def module(self): + return self._module + def full_text(self): return self._text diff --git a/themes/icons/test.json b/themes/icons/test.json new file mode 100644 index 0000000..ad17178 --- /dev/null +++ b/themes/icons/test.json @@ -0,0 +1,6 @@ +{ + "test-widget": { + "prefix": "widget-prefix", + "suffix": "widget-suffix" + } +} diff --git a/themes/test.json b/themes/test.json new file mode 100644 index 0000000..5f5cd00 --- /dev/null +++ b/themes/test.json @@ -0,0 +1,7 @@ +{ + "icons": [ "test" ], + "defaults": { + "prefix": "default-prefix", + "suffix": "default-suffix" + } +} From 2fa8d7b7786a9be80f93ca415272bb0a0fd65c6d Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Thu, 8 Dec 2016 12:44:52 +0100 Subject: [PATCH 025/104] [core/themes] Add module-specific themes Allow module-specific theme information to overload "default" configuration. I.e. it is now possible to have specific prefix or postfix configurations for different modules. The module name is derived for each widget from the module (__module__) from which it was instantiated. see #23 --- bumblebee/engine.py | 7 ++++++- bumblebee/output.py | 33 ++++++++++++++++++++------------- bumblebee/theme.py | 7 +++++++ tests/test_i3baroutput.py | 19 +++++++++++++++---- tests/util.py | 8 +++++++- themes/icons/awesome-fonts.json | 2 +- 6 files changed, 56 insertions(+), 20 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 2ba5107..d87e66f 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -13,6 +13,7 @@ class Module(object): this base class. """ def __init__(self, engine, widgets): + self.name = self.__module__.split(".")[-1] self._widgets = [] if widgets: self._widgets = widgets if isinstance(widgets, list) else [widgets] @@ -59,10 +60,14 @@ class Engine(object): """Start the event loop""" self._output.start() while self.running(): + self._output.begin() for module in self._modules: module.update(module.widgets()) - self._output.draw(widgets=module.widgets(), engine=self) + for widget in module.widgets(): + widget.set_module(module) + self._output.draw(widget=widget, engine=self) self._output.flush() + self._output.end() if self.running(): time.sleep(1) diff --git a/bumblebee/output.py b/bumblebee/output.py index 4b9603b..13b35c6 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -9,6 +9,13 @@ class Widget(object): """Represents a single visible block in the status bar""" def __init__(self, full_text): self._full_text = full_text + self._module = None + + def set_module(self, module): + self._module = module.name + + def module(self): + return self._module def full_text(self): """Retrieve the full text to display in the widget""" @@ -21,6 +28,7 @@ class I3BarOutput(object): """Manage output according to the i3bar protocol""" def __init__(self, theme): self._theme = theme + self._widgets = [] def start(self): """Print start preamble for i3bar protocol""" @@ -30,30 +38,29 @@ class I3BarOutput(object): """Finish i3bar protocol""" sys.stdout.write("]\n") - def draw_widget(self, result, widget): + def draw(self, widget, engine=None): """Draw a single widget""" full_text = widget.full_text() prefix = self._theme.prefix(widget) suffix = self._theme.suffix(widget) if prefix: - full_text = "{}{}".format(prefix, full_text) + full_text = u"{}{}".format(prefix, full_text) if suffix: - full_text = "{}{}".format(full_text, suffix) - result.append({ - u"full_text": "{}".format(full_text) + full_text = u"{}{}".format(full_text, suffix) + self._widgets.append({ + u"full_text": u"{}".format(full_text) }) - def draw(self, widgets, engine=None): - """Draw a number of widgets""" - if not isinstance(widgets, list): - widgets = [widgets] - result = [] - for widget in widgets: - self.draw_widget(result, widget) - sys.stdout.write(json.dumps(result)) + def begin(self): + """Start one output iteration""" + self._widgets = [] def flush(self): """Flushes output""" + sys.stdout.write(json.dumps(self._widgets)) + + def end(self): + """Finalizes output""" sys.stdout.write(",\n") sys.stdout.flush() diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 3b53f7d..380b20e 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -55,9 +55,16 @@ class Theme(object): module_theme = self._theme.get(widget.module(), {}) + padding = None + if name != "padding": + padding = self._get(widget, "padding") + value = self._defaults.get(name, default) value = module_theme.get(name, value) + if value and padding: + value = u"{}{}{}".format(padding, value, padding) + return value # algorithm copied from diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index e621611..55d6e10 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -33,25 +33,34 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_draw_single_widget(self, stdout): self.output.draw(self.someWidget) + self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], self.someWidget.full_text()) @mock.patch("sys.stdout", new_callable=StringIO) def test_draw_multiple_widgets(self, stdout): - self.output.draw([self.someWidget, self.someWidget]) + for widget in [self.someWidget, self.someWidget]: + self.output.draw(widget) + self.output.flush() result = json.loads(stdout.getvalue()) for res in result: - self.assertEquals(res["full_text"], self.someWidget.full_text()) + self.assertEquals(res["full_text"], widget.full_text()) @mock.patch("sys.stdout", new_callable=StringIO) - def test_flush(self, stdout): - self.output.flush() + def test_begin(self, stdout): + self.output.begin() + self.assertEquals("", stdout.getvalue()) + + @mock.patch("sys.stdout", new_callable=StringIO) + def test_end(self, stdout): + self.output.end() self.assertEquals(",\n", stdout.getvalue()) @mock.patch("sys.stdout", new_callable=StringIO) def test_prefix(self, stdout): self.theme.set_prefix(" - ") self.output.draw(self.someWidget) + self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( self.theme.prefix(self.someWidget), self.someWidget.full_text()) @@ -61,6 +70,7 @@ class TestI3BarOutput(unittest.TestCase): def test_suffix(self, stdout): self.theme.set_suffix(" - ") self.output.draw(self.someWidget) + self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( self.someWidget.full_text(), self.theme.suffix(self.someWidget)) @@ -71,6 +81,7 @@ class TestI3BarOutput(unittest.TestCase): self.theme.set_suffix(" - ") self.theme.set_prefix(" * ") self.output.draw(self.someWidget) + self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}{}".format( self.theme.prefix(self.someWidget), self.someWidget.full_text(), self.theme.suffix(self.someWidget)) diff --git a/tests/util.py b/tests/util.py index 28a562d..334664b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -13,12 +13,18 @@ class MockOutput(object): def stop(self): pass - def draw(self, widgets, engine): + def draw(self, widget, engine): engine.stop() + def begin(self): + pass + def flush(self): pass + def end(self): + pass + class MockWidget(object): def __init__(self, text): self._text = text diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 34abf24..3424710 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -1,5 +1,5 @@ { - "defaults": { "separator": "" }, + "defaults": { "separator": "", "padding": " " }, "date": { "prefix": "" }, "time": { "prefix": "" }, "memory": { "prefix": "" }, From f40418475f31653491c2006c834d7e5a5e89652e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 07:11:23 +0100 Subject: [PATCH 026/104] [modules/datetime] Re-enable datetime module Add datetime module + aliases date and time. see #23 --- bumblebee/engine.py | 4 ++++ bumblebee/modules/date.py | 1 + bumblebee/modules/datetime.py | 31 +++++++++++++++++++++++++++++++ bumblebee/modules/time.py | 1 + bumblebee/output.py | 5 +++++ bumblebee/theme.py | 5 ++++- tests/test_i3baroutput.py | 8 +++++--- 7 files changed, 51 insertions(+), 4 deletions(-) create mode 120000 bumblebee/modules/date.py create mode 100644 bumblebee/modules/datetime.py create mode 120000 bumblebee/modules/time.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index d87e66f..c5b6266 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -22,6 +22,10 @@ class Module(object): """Return the widgets to draw for this module""" return self._widgets + def update(self, widgets): + """By default, update() is a NOP""" + pass + class Engine(object): """Engine for driving the application diff --git a/bumblebee/modules/date.py b/bumblebee/modules/date.py new file mode 120000 index 0000000..bde6404 --- /dev/null +++ b/bumblebee/modules/date.py @@ -0,0 +1 @@ +datetime.py \ No newline at end of file diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py new file mode 100644 index 0000000..fc11a89 --- /dev/null +++ b/bumblebee/modules/datetime.py @@ -0,0 +1,31 @@ +# pylint: disable=C0111,R0903 + +"""Displays the current time, using the optional format string as input for strftime.""" + +from __future__ import absolute_import +import datetime +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): + super(Module, self).__init__(engine, + bumblebee.output.Widget(full_text=self.get_time) + ) + module = self.__module__.split(".")[-1] + + self._fmt = default_format(module) + +# self._fmt = self._config.parameter("format", default_format(module)) + + def get_time(self): + return datetime.datetime.now().strftime(self._fmt) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/time.py b/bumblebee/modules/time.py new file mode 120000 index 0000000..bde6404 --- /dev/null +++ b/bumblebee/modules/time.py @@ -0,0 +1 @@ +datetime.py \ No newline at end of file diff --git a/bumblebee/output.py b/bumblebee/output.py index 13b35c6..92bad91 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -12,9 +12,14 @@ class Widget(object): self._module = None def set_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 def module(self): + """Return the name of the module that spawned this widget""" return self._module def full_text(self): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 380b20e..2498655 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -31,10 +31,12 @@ class Theme(object): return self._get(widget, "suffix", None) def loads(self, data): + """Initialize the theme from a JSON string""" theme = json.loads(data) self._init(theme) def _load_icons(self, name): + """Load icons for a theme""" path = "{}/icons/".format(theme_path()) return self.load(name, path=path) @@ -52,7 +54,7 @@ class Theme(object): raise bumblebee.error.ThemeLoadError("no such theme: {}".format(name)) def _get(self, widget, name, default=None): - + """Return the config value 'name' for 'widget'""" module_theme = self._theme.get(widget.module(), {}) padding = None @@ -71,6 +73,7 @@ class Theme(object): # 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) diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 55d6e10..dbf2feb 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -44,7 +44,7 @@ class TestI3BarOutput(unittest.TestCase): self.output.flush() result = json.loads(stdout.getvalue()) for res in result: - self.assertEquals(res["full_text"], widget.full_text()) + self.assertEquals(res["full_text"], self.someWidget.full_text()) @mock.patch("sys.stdout", new_callable=StringIO) def test_begin(self, stdout): @@ -84,7 +84,9 @@ class TestI3BarOutput(unittest.TestCase): self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}{}".format( - self.theme.prefix(self.someWidget), self.someWidget.full_text(), self.theme.suffix(self.someWidget)) - ) + self.theme.prefix(self.someWidget), + self.someWidget.full_text(), + self.theme.suffix(self.someWidget) + )) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From a7e756e015cc37e1fbe2ef6b4e5258fa198a9060 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 07:27:01 +0100 Subject: [PATCH 027/104] [tests] Generic module tests Add a helper function that lists all existing modules and modify the CPU module test so that it now generically iterates all available modules and tests their widgets. see #23 --- bumblebee/engine.py | 13 +++++++++++++ tests/modules/test_cpu.py | 23 ----------------------- tests/modules/test_modules.py | 31 +++++++++++++++++++++++++++++++ tests/util.py | 3 +++ 4 files changed, 47 insertions(+), 23 deletions(-) delete mode 100644 tests/modules/test_cpu.py create mode 100644 tests/modules/test_modules.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index c5b6266..42a02e5 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -1,8 +1,21 @@ """Core application engine""" +import os import time +import pkgutil import importlib import bumblebee.error +import bumblebee.modules + +def 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 diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py deleted file mode 100644 index 9ec7ecb..0000000 --- a/tests/modules/test_cpu.py +++ /dev/null @@ -1,23 +0,0 @@ -# pylint: disable=C0103,C0111 - -import unittest - -from bumblebee.modules.cpu import Module -from tests.util import assertWidgetAttributes - -class TestCPUModule(unittest.TestCase): - def setUp(self): - self.module = Module(None) - - def test_widgets(self): - widgets = self.module.widgets() - for widget in widgets: - assertWidgetAttributes(self, widget) - - def test_update(self): - widgets = self.module.widgets() - self.module.update(widgets) - self.test_widgets() - self.assertEquals(widgets, self.module.widgets()) - -# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py new file mode 100644 index 0000000..7cdb48b --- /dev/null +++ b/tests/modules/test_modules.py @@ -0,0 +1,31 @@ +# pylint: disable=C0103,C0111 + +import unittest +import importlib + +from bumblebee.modules.cpu import Module +from bumblebee.engine import modules +from tests.util import assertWidgetAttributes, MockEngine + +class TestGenericModules(unittest.TestCase): + def setUp(self): + engine = MockEngine() + self.objects = {} + for mod in modules(): + cls = importlib.import_module("bumblebee.modules.{}".format(mod["name"])) + self.objects[mod["name"]] = getattr(cls, "Module")(engine) + + def test_widgets(self): + for mod in self.objects: + widgets = self.objects[mod].widgets() + for widget in widgets: + assertWidgetAttributes(self, widget) + + def test_update(self): + for mod in self.objects: + widgets = self.objects[mod].widgets() + self.objects[mod].update(widgets) + self.test_widgets() + self.assertEquals(widgets, self.objects[mod].widgets()) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 334664b..3779c1c 100644 --- a/tests/util.py +++ b/tests/util.py @@ -6,6 +6,9 @@ def assertWidgetAttributes(test, widget): test.assertTrue(isinstance(widget, Widget)) test.assertTrue(hasattr(widget, "full_text")) +class MockEngine(object): + pass + class MockOutput(object): def start(self): pass From c8a51b416f2affb1e6ef256fc254fd78d74d362f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 07:41:07 +0100 Subject: [PATCH 028/104] [core] Add "Store" interface Add an interface that allows arbitrary objects to store/retrieve arbitrary key/value pairs. This will be used for different purposes in the future: * Config class(es) can store user-defined parameters for modules * Widgets can store state * ??? see #23 --- bumblebee/store.py | 18 ++++++++++++++++++ tests/test_store.py | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 bumblebee/store.py create mode 100644 tests/test_store.py diff --git a/bumblebee/store.py b/bumblebee/store.py new file mode 100644 index 0000000..9492c1d --- /dev/null +++ b/bumblebee/store.py @@ -0,0 +1,18 @@ +"""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): + def __init__(self): + self._data = {} + + def set(self, key, value): + self._data[key] = value + + def get(self, key, default=None): + return self._data.get(key, default) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_store.py b/tests/test_store.py new file mode 100644 index 0000000..0b712f0 --- /dev/null +++ b/tests/test_store.py @@ -0,0 +1,24 @@ +# pylint: disable=C0103,C0111,W0703 + +import unittest + +from bumblebee.store import Store + +class TestStore(unittest.TestCase): + def setUp(self): + self.store = Store() + self.anyKey = "some-key" + self.anyValue = "some-value" + self.unsetKey = "invalid-key" + + def test_set_value(self): + self.store.set(self.anyKey, self.anyValue) + self.assertEquals(self.store.get(self.anyKey), self.anyValue) + + def test_get_invalid_value(self): + result = self.store.get(self.unsetKey) + self.assertEquals(result, None) + + def test_get_invalid_with_default_value(self): + result = self.store.get(self.unsetKey, self.anyValue) + self.assertEquals(result, self.anyValue) From f33711f49f49362838dd4e99ac1e898ecb48ea24 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 07:57:21 +0100 Subject: [PATCH 029/104] [core] Pass configuration parameters to modules User can now use -p = to pass configuration parameters to modules. For this, the module gets a "parameter()" method. Parameter keys are in the format . where is the name of the loaded module. This is either the name of the module itself (e.g. "cpu") or its alias, if the user specified it, for example: bumblebee-status -m cpu -p cpu.warning=90 vs. bumblebee-status -m cpu:test -p test.warning=90 see #23 --- bumblebee/config.py | 11 ++++++++++- bumblebee/engine.py | 21 ++++++++++++++++++--- tests/test_engine.py | 2 +- tests/test_module.py | 23 +++++++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/bumblebee/config.py b/bumblebee/config.py index 386f12f..2a72599 100644 --- a/bumblebee/config.py +++ b/bumblebee/config.py @@ -5,9 +5,11 @@ module parameters, etc.) to all other components """ import argparse +import bumblebee.store MODULE_HELP = "" THEME_HELP = "" +PARAMETER_HELP = "" def create_parser(): """Create the argument parser""" @@ -15,18 +17,25 @@ def create_parser(): parser.add_argument("-m", "--modules", nargs="+", default=[], help=MODULE_HELP) parser.add_argument("-t", "--theme", default="default", help=THEME_HELP) + parser.add_argument("-p", "--parameters", nargs="+", default=[], + help=PARAMETER_HELP) return parser -class Config(object): +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 []) + for param in self._args.parameters: + key, value = param.split("=") + self.set(key, value) + def modules(self): """Return a list of all activated modules""" return [{ diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 42a02e5..d219b2e 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -39,6 +39,16 @@ class Module(object): """By default, update() is a NOP""" pass + def parameter(self, name, default=None): + """Return the config parameter 'name' for this module""" + name = "{}.{}".format(self._config_name, name) + return self._config.get(name, default) + + def set_config(self, config, name): + """Set the config for this module""" + self._config = config + self._config_name = name + class Engine(object): """Engine for driving the application @@ -47,6 +57,7 @@ class Engine(object): """ def __init__(self, config, output=None): self._output = output + self._config = config self._running = True self._modules = [] self.load_modules(config.modules()) @@ -54,16 +65,20 @@ class Engine(object): def load_modules(self, modules): """Load specified modules and return them as list""" for module in modules: - self._modules.append(self.load_module(module["module"])) + self._modules.append(self.load_module(module["module"], module["name"])) return self._modules - def load_module(self, module_name): + def load_module(self, module_name, config_name=None): """Load specified module and return it as object""" + if config_name == None: + config_name = module_name try: module = importlib.import_module("bumblebee.modules.{}".format(module_name)) except ImportError as error: raise bumblebee.error.ModuleLoadError(error) - return getattr(module, "Module")(self) + res = getattr(module, "Module")(self) + res.set_config(self._config, config_name) + return res def running(self): """Check whether the event loop is running""" diff --git a/tests/test_engine.py b/tests/test_engine.py index 113e7ce..3aa2f6b 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -11,7 +11,7 @@ from tests.util import MockOutput class TestEngine(unittest.TestCase): def setUp(self): self.engine = Engine(config=Config(), output=MockOutput()) - self.singleWidgetModule = [{"module": "test"}] + self.singleWidgetModule = [{"module": "test", "name": "a"}] self.testModule = "test" self.invalidModule = "no-such-module" self.testModuleSpec = "bumblebee.modules.{}".format(self.testModule) diff --git a/tests/test_module.py b/tests/test_module.py index 34aa69d..a2e62a7 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -3,17 +3,32 @@ import unittest from bumblebee.engine import Module +from bumblebee.config import Config from tests.util import MockWidget class TestModule(unittest.TestCase): def setUp(self): self.widget = MockWidget("foo") + self.config = Config() self.moduleWithoutWidgets = Module(engine=None, widgets=None) self.moduleWithOneWidget = Module(engine=None, widgets=self.widget) self.moduleWithMultipleWidgets = Module(engine=None, widgets=[self.widget, self.widget, self.widget] ) + self.anyModule = Module(engine=None, widgets = self.widget) + self.anotherModule = Module(engine=None, widgets = self.widget) + self.anyConfigName = "cfg" + self.anotherConfigName = "cfg2" + self.anyKey = "some-parameter" + self.anyValue = "value" + self.anotherValue = "another-value" + self.emptyKey = "i-do-not-exist" + self.config.set("{}.{}".format(self.anyConfigName, self.anyKey), self.anyValue) + self.config.set("{}.{}".format(self.anotherConfigName, self.anyKey), self.anotherValue) + self.anyModule.set_config(self.config, self.anyConfigName) + self.anotherModule.set_config(self.config, self.anotherConfigName) + def test_empty_widgets(self): self.assertEquals(self.moduleWithoutWidgets.widgets(), []) @@ -23,3 +38,11 @@ class TestModule(unittest.TestCase): def test_multiple_widgets(self): for widget in self.moduleWithMultipleWidgets.widgets(): self.assertEquals(widget, self.widget) + + def test_parameters(self): + self.assertEquals(self.anyModule.parameter(self.anyKey), self.anyValue) + self.assertEquals(self.anotherModule.parameter(self.anyKey), self.anotherValue) + + def test_default_parameters(self): + self.assertEquals(self.anyModule.parameter(self.emptyKey), None) + self.assertEquals(self.anyModule.parameter(self.emptyKey, self.anyValue), self.anyValue) From 252260c249410af2f8b0c32feed9f926b46a9dae Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 08:23:53 +0100 Subject: [PATCH 030/104] [modules/datetime] Use parameter functionality to get format Make the format string of the datetime module configurable using the new parameter() method in the module. Also, restructured the setting of the config information a bit so that the parameter() method can be used in the constructor of a module. see #23 --- bumblebee/engine.py | 21 ++++++++++----------- bumblebee/modules/cpu.py | 4 ++-- bumblebee/modules/datetime.py | 9 +++------ bumblebee/modules/test.py | 4 ++-- tests/modules/test_modules.py | 4 +++- tests/test_module.py | 10 ++++++---- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index d219b2e..99cea34 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -25,8 +25,11 @@ class Module(object): (e.g. CPU utilization, disk usage, etc.) derive from this base class. """ - def __init__(self, engine, widgets): + def __init__(self, engine, config={}, widgets=[]): self.name = self.__module__.split(".")[-1] + self._config = config + if not "name" in self._config: + self._config["name"] = self.name self._widgets = [] if widgets: self._widgets = widgets if isinstance(widgets, list) else [widgets] @@ -41,13 +44,8 @@ class Module(object): def parameter(self, name, default=None): """Return the config parameter 'name' for this module""" - name = "{}.{}".format(self._config_name, name) - return self._config.get(name, default) - - def set_config(self, config, name): - """Set the config for this module""" - self._config = config - self._config_name = name + name = "{}.{}".format(self._config["name"], name) + return self._config["config"].get(name, default) class Engine(object): """Engine for driving the application @@ -76,9 +74,10 @@ class Engine(object): module = importlib.import_module("bumblebee.modules.{}".format(module_name)) except ImportError as error: raise bumblebee.error.ModuleLoadError(error) - res = getattr(module, "Module")(self) - res.set_config(self._config, config_name) - return res + return getattr(module, "Module")(self, { + "name": config_name, + "config": self._config + }) def running(self): """Check whether the event loop is running""" diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index dbee32e..2951ad8 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -6,8 +6,8 @@ import psutil import bumblebee.engine class Module(bumblebee.engine.Module): - def __init__(self, engine): - super(Module, self).__init__(engine, + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.utilization) ) self._utilization = psutil.cpu_percent(percpu=False) diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py index fc11a89..7d776ad 100644 --- a/bumblebee/modules/datetime.py +++ b/bumblebee/modules/datetime.py @@ -15,15 +15,12 @@ def default_format(module): return default class Module(bumblebee.engine.Module): - def __init__(self, engine): - super(Module, self).__init__(engine, + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.get_time) ) module = self.__module__.split(".")[-1] - - self._fmt = default_format(module) - -# self._fmt = self._config.parameter("format", default_format(module)) + self._fmt = self.parameter("format", default_format(module)) def get_time(self): return datetime.datetime.now().strftime(self._fmt) diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index 61af7e9..ad07faa 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -5,8 +5,8 @@ import bumblebee.engine class Module(bumblebee.engine.Module): - def __init__(self, engine): - super(Module, self).__init__(engine, + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text="test") ) diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 7cdb48b..d08e783 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -5,15 +5,17 @@ import importlib from bumblebee.modules.cpu import Module from bumblebee.engine import modules +from bumblebee.config import Config from tests.util import assertWidgetAttributes, MockEngine class TestGenericModules(unittest.TestCase): def setUp(self): engine = MockEngine() + config = Config() self.objects = {} for mod in modules(): cls = importlib.import_module("bumblebee.modules.{}".format(mod["name"])) - self.objects[mod["name"]] = getattr(cls, "Module")(engine) + self.objects[mod["name"]] = getattr(cls, "Module")(engine, { "config": config }) def test_widgets(self): for mod in self.objects: diff --git a/tests/test_module.py b/tests/test_module.py index a2e62a7..44a39af 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -16,18 +16,20 @@ class TestModule(unittest.TestCase): widgets=[self.widget, self.widget, self.widget] ) - self.anyModule = Module(engine=None, widgets = self.widget) - self.anotherModule = Module(engine=None, widgets = self.widget) self.anyConfigName = "cfg" self.anotherConfigName = "cfg2" + self.anyModule = Module(engine=None, widgets=self.widget, config={ + "name": self.anyConfigName, "config": self.config + }) + self.anotherModule = Module(engine=None, widgets=self.widget, config={ + "name": self.anotherConfigName, "config": self.config + }) self.anyKey = "some-parameter" self.anyValue = "value" self.anotherValue = "another-value" self.emptyKey = "i-do-not-exist" self.config.set("{}.{}".format(self.anyConfigName, self.anyKey), self.anyValue) self.config.set("{}.{}".format(self.anotherConfigName, self.anyKey), self.anotherValue) - self.anyModule.set_config(self.config, self.anyConfigName) - self.anotherModule.set_config(self.config, self.anotherConfigName) def test_empty_widgets(self): self.assertEquals(self.moduleWithoutWidgets.widgets(), []) From 0c7884d1708d4ca554393e0be08384d1f62f4c21 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 08:43:14 +0100 Subject: [PATCH 031/104] [all] pylint refinements Improve code by bringing up the pylint score a bit. see #23 --- bumblebee/engine.py | 6 +++--- bumblebee/store.py | 3 +++ tests/modules/test_modules.py | 7 +++---- tests/test_module.py | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 99cea34..ca4bb55 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -7,7 +7,7 @@ import importlib import bumblebee.error import bumblebee.modules -def modules(): +def all_modules(): """Return a list of available modules""" result = [] path = os.path.dirname(bumblebee.modules.__file__) @@ -28,7 +28,7 @@ class Module(object): def __init__(self, engine, config={}, widgets=[]): self.name = self.__module__.split(".")[-1] self._config = config - if not "name" in self._config: + if "name" not in self._config: self._config["name"] = self.name self._widgets = [] if widgets: @@ -68,7 +68,7 @@ class Engine(object): def load_module(self, module_name, config_name=None): """Load specified module and return it as object""" - if config_name == None: + if config_name is None: config_name = module_name try: module = importlib.import_module("bumblebee.modules.{}".format(module_name)) diff --git a/bumblebee/store.py b/bumblebee/store.py index 9492c1d..8fac71b 100644 --- a/bumblebee/store.py +++ b/bumblebee/store.py @@ -6,13 +6,16 @@ this module """ class Store(object): + """Interface for storing and retrieving simple values""" def __init__(self): self._data = {} def set(self, key, value): + """Set 'key' to 'value', overwriting 'key' if it exists already""" self._data[key] = value def get(self, key, default=None): + """Return the current value of 'key', or 'default' if 'key' is not set""" return self._data.get(key, default) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index d08e783..baa6233 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -3,8 +3,7 @@ import unittest import importlib -from bumblebee.modules.cpu import Module -from bumblebee.engine import modules +from bumblebee.engine import all_modules from bumblebee.config import Config from tests.util import assertWidgetAttributes, MockEngine @@ -13,9 +12,9 @@ class TestGenericModules(unittest.TestCase): engine = MockEngine() config = Config() self.objects = {} - for mod in modules(): + for mod in all_modules(): cls = importlib.import_module("bumblebee.modules.{}".format(mod["name"])) - self.objects[mod["name"]] = getattr(cls, "Module")(engine, { "config": config }) + self.objects[mod["name"]] = getattr(cls, "Module")(engine, {"config": config}) def test_widgets(self): for mod in self.objects: diff --git a/tests/test_module.py b/tests/test_module.py index 44a39af..0cc95ad 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -19,7 +19,7 @@ class TestModule(unittest.TestCase): self.anyConfigName = "cfg" self.anotherConfigName = "cfg2" self.anyModule = Module(engine=None, widgets=self.widget, config={ - "name": self.anyConfigName, "config": self.config + "name": self.anyConfigName, "config": self.config }) self.anotherModule = Module(engine=None, widgets=self.widget, config={ "name": self.anotherConfigName, "config": self.config From c52cb995183daef87d3e4b45f54a648efd353d17 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 08:58:45 +0100 Subject: [PATCH 032/104] [core/theme] Add support for foreground and background colors Themes can now define "fg" and "bg" attributes that are used for foreground (text) color and background color. see #23 --- bumblebee/output.py | 4 +++- bumblebee/theme.py | 8 ++++++++ tests/test_i3baroutput.py | 12 ++++++++++++ tests/test_theme.py | 14 ++++++++++++++ tests/util.py | 14 ++++++++++++++ themes/test.json | 8 +++++++- 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index 92bad91..5eba197 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -53,7 +53,9 @@ class I3BarOutput(object): if suffix: full_text = u"{}{}".format(full_text, suffix) self._widgets.append({ - u"full_text": u"{}".format(full_text) + u"full_text": u"{}".format(full_text), + "color": self._theme.fg(widget), + "background": self._theme.bg(widget), }) def begin(self): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 2498655..28fe5b3 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -30,6 +30,14 @@ class Theme(object): """Return the theme suffix for a widget's full text""" return self._get(widget, "suffix", None) + 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 loads(self, data): """Initialize the theme from a JSON string""" theme = json.loads(data) diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index dbf2feb..a836247 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -19,6 +19,8 @@ class TestI3BarOutput(unittest.TestCase): self.expectedStart = json.dumps({"version": 1, "click_events": True}) + "[\n" self.expectedStop = "]\n" self.someWidget = MockWidget("foo bar baz") + self.anyColor = "#ababab" + self.anotherColor = "#cccccc" @mock.patch("sys.stdout", new_callable=StringIO) def test_start(self, stdout): @@ -89,4 +91,14 @@ class TestI3BarOutput(unittest.TestCase): self.theme.suffix(self.someWidget) )) + @mock.patch("sys.stdout", new_callable=StringIO) + def test_colors(self, stdout): + self.theme.set_fg(self.anyColor) + self.theme.set_bg(self.anotherColor) + self.output.draw(self.someWidget) + self.output.flush() + result = json.loads(stdout.getvalue())[0] + self.assertEquals(result["color"], self.anyColor) + self.assertEquals(result["background"], self.anotherColor) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_theme.py b/tests/test_theme.py index a956d4f..51b10a9 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -14,10 +14,14 @@ class TestTheme(unittest.TestCase): self.theme = Theme(self.validThemeName) self.widgetTheme = "test-widget" + self.defaultColor = "#000000" + self.defaultBgColor = "#111111" + self.widgetBgColor = "#222222" self.defaultPrefix = "default-prefix" self.defaultSuffix = "default-suffix" self.widgetPrefix = "widget-prefix" self.widgetSuffix = "widget-suffix" + self.widgetColor = "#ababab" def test_load_valid_theme(self): try: @@ -43,4 +47,14 @@ class TestTheme(unittest.TestCase): self.someWidget.set_module(self.widgetTheme) self.assertEquals(self.theme.prefix(self.someWidget), self.widgetPrefix) + def test_widget_fg(self): + self.assertEquals(self.theme.fg(self.someWidget), self.defaultColor) + self.someWidget.set_module(self.widgetTheme) + self.assertEquals(self.theme.fg(self.someWidget), self.widgetColor) + + def test_widget_bg(self): + self.assertEquals(self.theme.bg(self.someWidget), self.defaultBgColor) + self.someWidget.set_module(self.widgetTheme) + self.assertEquals(self.theme.bg(self.someWidget), self.widgetBgColor) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 3779c1c..564fe54 100644 --- a/tests/util.py +++ b/tests/util.py @@ -49,6 +49,8 @@ class MockTheme(object): def __init__(self): self._prefix = None self._suffix = None + self._fg = None + self._bg = None def set_prefix(self, value): self._prefix = value @@ -56,10 +58,22 @@ class MockTheme(object): def set_suffix(self, value): self._suffix = value + def set_fg(self, value): + self._fg = value + + def set_bg(self, value): + self._bg = value + def prefix(self, widget): return self._prefix def suffix(self, widget): return self._suffix + def fg(self, widget): + return self._fg + + def bg(self, widget): + return self._bg + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/test.json b/themes/test.json index 5f5cd00..ea713bd 100644 --- a/themes/test.json +++ b/themes/test.json @@ -2,6 +2,12 @@ "icons": [ "test" ], "defaults": { "prefix": "default-prefix", - "suffix": "default-suffix" + "suffix": "default-suffix", + "fg": "#000000", + "bg": "#111111" + }, + "test-widget": { + "fg": "#ababab", + "bg": "#222222" } } From 068968bbf557d20977d87c9bfb029c60f9a3c976 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 11:42:02 +0100 Subject: [PATCH 033/104] [tests] Refactor by removing getter/setter methods Instead of having get/set methods for simple attributes, use the attributes directly. see #23 --- tests/test_i3baroutput.py | 12 ++++++------ tests/test_theme.py | 6 +++--- tests/util.py | 35 ++++++++++------------------------- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index a836247..60ed671 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -60,7 +60,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_prefix(self, stdout): - self.theme.set_prefix(" - ") + self.theme.attr_prefix = " - " self.output.draw(self.someWidget) self.output.flush() result = json.loads(stdout.getvalue())[0] @@ -70,7 +70,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_suffix(self, stdout): - self.theme.set_suffix(" - ") + self.theme.attr_suffix = " - " self.output.draw(self.someWidget) self.output.flush() result = json.loads(stdout.getvalue())[0] @@ -80,8 +80,8 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_bothfix(self, stdout): - self.theme.set_suffix(" - ") - self.theme.set_prefix(" * ") + self.theme.attr_suffix = " - " + self.theme.attr_prefix = " * " self.output.draw(self.someWidget) self.output.flush() result = json.loads(stdout.getvalue())[0] @@ -93,8 +93,8 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_colors(self, stdout): - self.theme.set_fg(self.anyColor) - self.theme.set_bg(self.anotherColor) + self.theme.attr_fg = self.anyColor + self.theme.attr_bg = self.anotherColor self.output.draw(self.someWidget) self.output.flush() result = json.loads(stdout.getvalue())[0] diff --git a/tests/test_theme.py b/tests/test_theme.py index 51b10a9..3b5690a 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -44,17 +44,17 @@ class TestTheme(unittest.TestCase): self.assertEquals(self.theme.suffix(self.someWidget), self.defaultSuffix) def test_widget_prefix(self): - self.someWidget.set_module(self.widgetTheme) + self.someWidget.attr_module = self.widgetTheme self.assertEquals(self.theme.prefix(self.someWidget), self.widgetPrefix) def test_widget_fg(self): self.assertEquals(self.theme.fg(self.someWidget), self.defaultColor) - self.someWidget.set_module(self.widgetTheme) + self.someWidget.attr_module = self.widgetTheme self.assertEquals(self.theme.fg(self.someWidget), self.widgetColor) def test_widget_bg(self): self.assertEquals(self.theme.bg(self.someWidget), self.defaultBgColor) - self.someWidget.set_module(self.widgetTheme) + self.someWidget.attr_module = self.widgetTheme self.assertEquals(self.theme.bg(self.someWidget), self.widgetBgColor) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 564fe54..7eb73ce 100644 --- a/tests/util.py +++ b/tests/util.py @@ -31,49 +31,34 @@ class MockOutput(object): class MockWidget(object): def __init__(self, text): self._text = text - self._module = None - - def set_module(self, name): - self._module = name + self.attr_module = None def update(self, widgets): pass def module(self): - return self._module + return self.attr_module def full_text(self): return self._text class MockTheme(object): def __init__(self): - self._prefix = None - self._suffix = None - self._fg = None - self._bg = None - - def set_prefix(self, value): - self._prefix = value - - def set_suffix(self, value): - self._suffix = value - - def set_fg(self, value): - self._fg = value - - def set_bg(self, value): - self._bg = value + self.attr_prefix = None + self.attr_suffix = None + self.attr_fg = None + self.attr_bg = None def prefix(self, widget): - return self._prefix + return self.attr_prefix def suffix(self, widget): - return self._suffix + return self.attr_suffix def fg(self, widget): - return self._fg + return self.attr_fg def bg(self, widget): - return self._bg + return self.attr_bg # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From e59e969bdc0a9fbef3f6f296b02d9ac6ccf686de Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 11:49:59 +0100 Subject: [PATCH 034/104] [core] Refactor -> replace some getter/setter pairs with attributes Remove some set_* methods and replace them with a simple attribute. see #23 --- bumblebee/engine.py | 6 +++--- bumblebee/output.py | 10 +++------- bumblebee/theme.py | 2 +- tests/modules/test_modules.py | 2 ++ tests/test_engine.py | 6 +++--- tests/test_theme.py | 6 +++--- tests/util.py | 5 +---- 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index ca4bb55..463d244 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -63,10 +63,10 @@ class Engine(object): def load_modules(self, modules): """Load specified modules and return them as list""" for module in modules: - self._modules.append(self.load_module(module["module"], module["name"])) + self._modules.append(self._load_module(module["module"], module["name"])) return self._modules - def load_module(self, module_name, config_name=None): + def _load_module(self, module_name, config_name=None): """Load specified module and return it as object""" if config_name is None: config_name = module_name @@ -95,7 +95,7 @@ class Engine(object): for module in self._modules: module.update(module.widgets()) for widget in module.widgets(): - widget.set_module(module) + widget.link_module(module) self._output.draw(widget=widget, engine=self) self._output.flush() self._output.end() diff --git a/bumblebee/output.py b/bumblebee/output.py index 5eba197..5d9e0df 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -9,18 +9,14 @@ class Widget(object): """Represents a single visible block in the status bar""" def __init__(self, full_text): self._full_text = full_text - self._module = None + self.module = None - def set_module(self, module): + 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 - - def module(self): - """Return the name of the module that spawned this widget""" - return self._module + self.module = module.name def full_text(self): """Retrieve the full text to display in the widget""" diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 28fe5b3..a8fa6c5 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -63,7 +63,7 @@ class Theme(object): def _get(self, widget, name, default=None): """Return the config value 'name' for 'widget'""" - module_theme = self._theme.get(widget.module(), {}) + module_theme = self._theme.get(widget.module, {}) padding = None if name != "padding": diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index baa6233..60c01e2 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -20,6 +20,8 @@ class TestGenericModules(unittest.TestCase): for mod in self.objects: widgets = self.objects[mod].widgets() for widget in widgets: + widget.link_module(self.objects[mod]) + self.assertEquals(widget.module, mod) assertWidgetAttributes(self, widget) def test_update(self): diff --git a/tests/test_engine.py b/tests/test_engine.py index 3aa2f6b..29729f6 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -26,16 +26,16 @@ class TestEngine(unittest.TestCase): self.assertFalse(self.engine.running()) def test_load_module(self): - module = self.engine.load_module(self.testModule) + module = self.engine._load_module(self.testModule) self.assertEquals(module.__module__, self.testModuleSpec) def test_load_invalid_module(self): with self.assertRaises(ModuleLoadError): - self.engine.load_module(self.invalidModule) + self.engine._load_module(self.invalidModule) def test_load_none(self): with self.assertRaises(ModuleLoadError): - self.engine.load_module(None) + self.engine._load_module(None) def test_load_modules(self): modules = self.engine.load_modules(self.testModules) diff --git a/tests/test_theme.py b/tests/test_theme.py index 3b5690a..4ae9b50 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -44,17 +44,17 @@ class TestTheme(unittest.TestCase): self.assertEquals(self.theme.suffix(self.someWidget), self.defaultSuffix) def test_widget_prefix(self): - self.someWidget.attr_module = self.widgetTheme + self.someWidget.module = self.widgetTheme self.assertEquals(self.theme.prefix(self.someWidget), self.widgetPrefix) def test_widget_fg(self): self.assertEquals(self.theme.fg(self.someWidget), self.defaultColor) - self.someWidget.attr_module = self.widgetTheme + self.someWidget.module = self.widgetTheme self.assertEquals(self.theme.fg(self.someWidget), self.widgetColor) def test_widget_bg(self): self.assertEquals(self.theme.bg(self.someWidget), self.defaultBgColor) - self.someWidget.attr_module = self.widgetTheme + self.someWidget.module = self.widgetTheme self.assertEquals(self.theme.bg(self.someWidget), self.widgetBgColor) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 7eb73ce..33a65d8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -31,14 +31,11 @@ class MockOutput(object): class MockWidget(object): def __init__(self, text): self._text = text - self.attr_module = None + self.module = None def update(self, widgets): pass - def module(self): - return self.attr_module - def full_text(self): return self._text From 59fb47ae3b52642dd0dffabb91324e47961c836c Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 11:57:13 +0100 Subject: [PATCH 035/104] [all] pylint cleanup --- bumblebee/theme.py | 2 ++ tests/test_engine.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bumblebee/theme.py b/bumblebee/theme.py index a8fa6c5..8d90b3f 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -1,3 +1,5 @@ +# pylint: disable=C0103 + """Theme support""" import os diff --git a/tests/test_engine.py b/tests/test_engine.py index 29729f6..f48feb4 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,4 +1,4 @@ -# pylint: disable=C0103,C0111,W0703 +# pylint: disable=C0103,C0111,W0703,W0212 import unittest From 527489e0de27ff5057155a980cf47782a9deb82e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 12:28:39 +0100 Subject: [PATCH 036/104] [core/themes] Add "cycling" support Allow a theme to define a "cycle" of attributes that are cycled through on a widget-per-widget basis (e.g. for alternating the widget background). These cycles take precedence over the default values, but can be overridden by module-specific theme instructions. see #23 --- bumblebee/output.py | 1 + bumblebee/theme.py | 38 ++++++++++++++++++------ tests/test_theme.py | 51 +++++++++++++++++++++------------ tests/util.py | 3 ++ themes/solarized-powerline.json | 8 +++--- themes/test_cycle.json | 18 ++++++++++++ 6 files changed, 87 insertions(+), 32 deletions(-) create mode 100644 themes/test_cycle.json diff --git a/bumblebee/output.py b/bumblebee/output.py index 5d9e0df..d3ea0f4 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -57,6 +57,7 @@ class I3BarOutput(object): def begin(self): """Start one output iteration""" self._widgets = [] + self._theme.reset() def flush(self): """Flushes output""" diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 8d90b3f..a4d40c6 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -16,6 +16,7 @@ class Theme(object): """Represents a collection of icons and colors""" def __init__(self, name): self._init(self.load(name)) + self._widget = None def _init(self, data): """Initialize theme from data structure""" @@ -23,14 +24,30 @@ class Theme(object): self._merge(data, self._load_icons(iconset)) self._theme = data 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 def prefix(self, widget): """Return the theme prefix for a widget's full text""" - return self._get(widget, "prefix", None) + padding = self._get(widget, "padding", "") + pre = self._get(widget, "prefix", None) + return u"{}{}{}".format(padding, pre, padding) if pre else None def suffix(self, widget): """Return the theme suffix for a widget's full text""" - return self._get(widget, "suffix", None) + padding = self._get(widget, "padding", "") + suf = self._get(widget, "suffix", None) + return u"{}{}{}".format(padding, suf, padding) if suf else None def fg(self, widget): """Return the foreground color for this widget""" @@ -65,18 +82,21 @@ class Theme(object): def _get(self, widget, name, default=None): """Return the config value 'name' for 'widget'""" + + if not self._widget: + self._widget = widget + + if self._widget != widget: + self._widget = widget + self._cycle_idx = (self._cycle_idx + 1) % len(self._cycles) + self._cycle = self._cycles[self._cycle_idx] + module_theme = self._theme.get(widget.module, {}) - padding = None - if name != "padding": - padding = self._get(widget, "padding") - value = self._defaults.get(name, default) + value = self._cycle.get(name, value) value = module_theme.get(name, value) - if value and padding: - value = u"{}{}{}".format(padding, value, padding) - return value # algorithm copied from diff --git a/tests/test_theme.py b/tests/test_theme.py index 4ae9b50..b039d17 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -10,18 +10,22 @@ class TestTheme(unittest.TestCase): self.nonexistentThemeName = "no-such-theme" self.invalidThemeName = "invalid" self.validThemeName = "test" - self.someWidget = MockWidget("foo") + self.themedWidget = MockWidget("foo") self.theme = Theme(self.validThemeName) + self.cycleTheme = Theme("test_cycle") + self.anyWidget = MockWidget("bla") + self.anotherWidget = MockWidget("blub") + data = self.theme.data() self.widgetTheme = "test-widget" - self.defaultColor = "#000000" - self.defaultBgColor = "#111111" - self.widgetBgColor = "#222222" - self.defaultPrefix = "default-prefix" - self.defaultSuffix = "default-suffix" - self.widgetPrefix = "widget-prefix" - self.widgetSuffix = "widget-suffix" - self.widgetColor = "#ababab" + self.defaultColor = data["defaults"]["fg"] + self.defaultBgColor = data["defaults"]["bg"] + self.widgetColor = data[self.widgetTheme]["fg"] + self.widgetBgColor = data[self.widgetTheme]["bg"] + self.defaultPrefix = data["defaults"]["prefix"] + self.defaultSuffix = data["defaults"]["suffix"] + self.widgetPrefix = data[self.widgetTheme]["prefix"] + self.widgetSuffix = data[self.widgetTheme]["suffix"] def test_load_valid_theme(self): try: @@ -38,23 +42,32 @@ class TestTheme(unittest.TestCase): Theme(self.invalidThemeName) def test_default_prefix(self): - self.assertEquals(self.theme.prefix(self.someWidget), self.defaultPrefix) + self.assertEquals(self.theme.prefix(self.themedWidget), self.defaultPrefix) def test_default_suffix(self): - self.assertEquals(self.theme.suffix(self.someWidget), self.defaultSuffix) + self.assertEquals(self.theme.suffix(self.themedWidget), self.defaultSuffix) def test_widget_prefix(self): - self.someWidget.module = self.widgetTheme - self.assertEquals(self.theme.prefix(self.someWidget), self.widgetPrefix) + self.themedWidget.module = self.widgetTheme + self.assertEquals(self.theme.prefix(self.themedWidget), self.widgetPrefix) def test_widget_fg(self): - self.assertEquals(self.theme.fg(self.someWidget), self.defaultColor) - self.someWidget.module = self.widgetTheme - self.assertEquals(self.theme.fg(self.someWidget), self.widgetColor) + self.assertEquals(self.theme.fg(self.themedWidget), self.defaultColor) + self.themedWidget.module = self.widgetTheme + self.assertEquals(self.theme.fg(self.themedWidget), self.widgetColor) def test_widget_bg(self): - self.assertEquals(self.theme.bg(self.someWidget), self.defaultBgColor) - self.someWidget.module = self.widgetTheme - self.assertEquals(self.theme.bg(self.someWidget), self.widgetBgColor) + self.assertEquals(self.theme.bg(self.themedWidget), self.defaultBgColor) + self.themedWidget.module = self.widgetTheme + self.assertEquals(self.theme.bg(self.themedWidget), self.widgetBgColor) + + def test_reset(self): + theme = self.cycleTheme + data = theme.data() + theme.reset() + self.assertEquals(theme.fg(self.anyWidget), data["cycle"][0]["fg"]) + self.assertEquals(theme.fg(self.anotherWidget), data["cycle"][1]["fg"]) + theme.reset() + self.assertEquals(theme.fg(self.anyWidget), data["cycle"][0]["fg"]) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 33a65d8..3a8095d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -46,6 +46,9 @@ class MockTheme(object): self.attr_fg = None self.attr_bg = None + def reset(self): + pass + def prefix(self, widget): return self.attr_prefix diff --git a/themes/solarized-powerline.json b/themes/solarized-powerline.json index 1326777..22b6faf 100644 --- a/themes/solarized-powerline.json +++ b/themes/solarized-powerline.json @@ -3,10 +3,6 @@ "defaults": { "default-separators": false, "separator-block-width": 0, - "cycle": [ - { "fg": "#93a1a1", "bg": "#002b36" }, - { "fg": "#eee8d5", "bg": "#586e75" } - ], "warning": { "fg": "#002b36", "bg": "#b58900" @@ -16,6 +12,10 @@ "bg": "#dc322f" } }, + "cycle": [ + { "fg": "#93a1a1", "bg": "#002b36" }, + { "fg": "#eee8d5", "bg": "#586e75" } + ], "dnf": { "good": { "fg": "#002b36", diff --git a/themes/test_cycle.json b/themes/test_cycle.json new file mode 100644 index 0000000..5fd7e1a --- /dev/null +++ b/themes/test_cycle.json @@ -0,0 +1,18 @@ +{ + "icons": [ "test" ], + "defaults": { + "prefix": "default-prefix", + "suffix": "default-suffix", + "fg": "#000000", + "bg": "#111111" + }, + "cycle": [ + { "fg": "#aa0000" }, + { "fg": "#00aa00" }, + { "fg": "#0000aa" } + ], + "test-widget": { + "fg": "#ababab", + "bg": "#222222" + } +} From c1f1e1a9396b40de837d5803ca372c02782640a0 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 12:55:16 +0100 Subject: [PATCH 037/104] [core/themes] Add separator customization Add customized separators: * The default separators are automatically disabled if custom separators are used (to "just" disable the default, use empty custom separators) * Use previous background color as their background color and the current background color as foreground color * Allow the separator-block-width to be configured see #23 --- bumblebee/output.py | 13 ++++++++++++- bumblebee/theme.py | 21 +++++++++++++++++++-- tests/test_theme.py | 24 ++++++++++++++++++++++++ tests/util.py | 8 ++++++++ themes/solarized-powerline.json | 1 - themes/test.json | 4 +++- 6 files changed, 66 insertions(+), 5 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index d3ea0f4..ace407f 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -48,10 +48,21 @@ class I3BarOutput(object): 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), + }) self._widgets.append({ - u"full_text": u"{}".format(full_text), + 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, }) def begin(self): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index a4d40c6..6e328e0 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -36,6 +36,7 @@ class Theme(object): self._cycle = self._cycles[0] if len(self._cycles) > 0 else {} self._cycle_idx = 0 self._widget = None + self._prevbg = None def prefix(self, widget): """Return the theme prefix for a widget's full text""" @@ -57,6 +58,20 @@ class Theme(object): """Return the background color for this widget""" return self._get(widget, "bg", None) + def separator(self, widget): + """Return the separator between widgets""" + return self._get(widget, "separator", None) + + def separator_fg(self, widget): + return self.bg(widget) + + def separator_bg(self, widget): + return self._prevbg + + def separator_block_width(self, widget): + """Return the SBW""" + return self._get(widget, "separator-block-width", None) + def loads(self, data): """Initialize the theme from a JSON string""" theme = json.loads(data) @@ -87,9 +102,11 @@ class Theme(object): self._widget = widget if self._widget != widget: + self._prevbg = self.bg(self._widget) self._widget = widget - self._cycle_idx = (self._cycle_idx + 1) % len(self._cycles) - self._cycle = self._cycles[self._cycle_idx] + 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, {}) diff --git a/tests/test_theme.py b/tests/test_theme.py index b039d17..d2220cd 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -61,6 +61,14 @@ class TestTheme(unittest.TestCase): self.themedWidget.module = self.widgetTheme self.assertEquals(self.theme.bg(self.themedWidget), self.widgetBgColor) + def test_absent_cycle(self): + theme = self.theme + try: + theme.fg(self.anyWidget) + theme.fg(self.anotherWidget) + except Exception as e: + self.fail(e) + def test_reset(self): theme = self.cycleTheme data = theme.data() @@ -70,4 +78,20 @@ class TestTheme(unittest.TestCase): theme.reset() self.assertEquals(theme.fg(self.anyWidget), data["cycle"][0]["fg"]) + def test_separator_block_width(self): + theme = self.theme + data = theme.data() + + self.assertEquals(theme.separator_block_width(self.anyWidget), data["defaults"]["separator-block-width"]) + + def test_separator(self): + for theme in [self.theme, self.cycleTheme]: + data = theme.data() + theme.reset() + prev_bg = theme.bg(self.anyWidget) + theme.bg(self.anotherWidget) + + self.assertEquals(theme.separator_fg(self.anotherWidget), theme.bg(self.anotherWidget)) + self.assertEquals(theme.separator_bg(self.anotherWidget), prev_bg) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 3a8095d..0d78aef 100644 --- a/tests/util.py +++ b/tests/util.py @@ -45,10 +45,18 @@ class MockTheme(object): self.attr_suffix = None self.attr_fg = None self.attr_bg = None + self.attr_separator = None + self.attr_separator_block_width = 0 def reset(self): pass + def separator_block_width(self, widget): + return self.attr_separator_block_width + + def separator(self, widget): + return self.attr_separator + def prefix(self, widget): return self.attr_prefix diff --git a/themes/solarized-powerline.json b/themes/solarized-powerline.json index 22b6faf..cb9049b 100644 --- a/themes/solarized-powerline.json +++ b/themes/solarized-powerline.json @@ -1,7 +1,6 @@ { "icons": [ "awesome-fonts" ], "defaults": { - "default-separators": false, "separator-block-width": 0, "warning": { "fg": "#002b36", diff --git a/themes/test.json b/themes/test.json index ea713bd..4640634 100644 --- a/themes/test.json +++ b/themes/test.json @@ -4,7 +4,9 @@ "prefix": "default-prefix", "suffix": "default-suffix", "fg": "#000000", - "bg": "#111111" + "bg": "#111111", + "separator": " * ", + "separator-block-width": 10 }, "test-widget": { "fg": "#ababab", From 88b36417f817fe8fa55bb81db94a69536577d519 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 13:06:08 +0100 Subject: [PATCH 038/104] [core/theme] Fix padding Missing prefix/suffix broke padding. see #23 --- bumblebee/output.py | 5 +++-- bumblebee/theme.py | 18 +++++++++++------- tests/util.py | 7 +++++-- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index ace407f..de80d90 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -42,8 +42,9 @@ class I3BarOutput(object): def draw(self, widget, engine=None): """Draw a single widget""" full_text = widget.full_text() - prefix = self._theme.prefix(widget) - suffix = self._theme.suffix(widget) + padding = self._theme.padding(widget) + prefix = self._theme.prefix(widget, padding) + suffix = self._theme.suffix(widget, padding) if prefix: full_text = u"{}{}".format(prefix, full_text) if suffix: diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 6e328e0..196a574 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -38,17 +38,21 @@ class Theme(object): self._widget = None self._prevbg = None - def prefix(self, widget): - """Return the theme prefix for a widget's full text""" - padding = self._get(widget, "padding", "") - pre = self._get(widget, "prefix", None) - return u"{}{}{}".format(padding, pre, padding) if pre else None + def padding(self, widget): + """Return padding for widget""" + return self._get(widget, "padding", "") - def suffix(self, widget): + 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 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 None + return u"{}{}{}".format(padding, suf, padding) if suf else default def fg(self, widget): """Return the foreground color for this widget""" diff --git a/tests/util.py b/tests/util.py index 0d78aef..314cfa0 100644 --- a/tests/util.py +++ b/tests/util.py @@ -48,6 +48,9 @@ class MockTheme(object): self.attr_separator = None self.attr_separator_block_width = 0 + def padding(self, widget): + return "" + def reset(self): pass @@ -57,10 +60,10 @@ class MockTheme(object): def separator(self, widget): return self.attr_separator - def prefix(self, widget): + def prefix(self, widget, default=None): return self.attr_prefix - def suffix(self, widget): + def suffix(self, widget, default=None): return self.attr_suffix def fg(self, widget): From 4baf63f88c82bb01c1b337653f6d46f88e0d276b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 13:32:22 +0100 Subject: [PATCH 039/104] [core/themes] Add state themes Each widget can now return a state using the method "state()". This string is then used to look up a theme information which is used instead of the default or module theme, if found. see #23 --- bumblebee/output.py | 3 +++ bumblebee/theme.py | 6 ++++++ tests/test_theme.py | 37 +++++++++++++++++++++++++++---------- tests/util.py | 6 +++++- themes/test.json | 11 +++++++++-- 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index de80d90..e582d34 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -18,6 +18,9 @@ class Widget(object): pass in the module name in every concrete module implementation""" self.module = module.name + def state(self): + return "state-default" + def full_text(self): """Retrieve the full text to display in the widget""" if callable(self._full_text): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 196a574..ddcd249 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -113,10 +113,16 @@ class Theme(object): self._cycle = self._cycles[self._cycle_idx] module_theme = self._theme.get(widget.module, {}) + if name != widget.state(): + # avoid infinite recursion + state_theme = self._get(widget, widget.state(), {}) + else: + state_theme = {} value = self._defaults.get(name, default) value = self._cycle.get(name, value) value = module_theme.get(name, value) + value = state_theme.get(name, value) return value diff --git a/tests/test_theme.py b/tests/test_theme.py index d2220cd..75e7bc6 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -10,7 +10,7 @@ class TestTheme(unittest.TestCase): self.nonexistentThemeName = "no-such-theme" self.invalidThemeName = "invalid" self.validThemeName = "test" - self.themedWidget = MockWidget("foo") + self.themedWidget = MockWidget("bla") self.theme = Theme(self.validThemeName) self.cycleTheme = Theme("test_cycle") self.anyWidget = MockWidget("bla") @@ -18,6 +18,7 @@ class TestTheme(unittest.TestCase): data = self.theme.data() self.widgetTheme = "test-widget" + self.themedWidget.module = self.widgetTheme self.defaultColor = data["defaults"]["fg"] self.defaultBgColor = data["defaults"]["bg"] self.widgetColor = data[self.widgetTheme]["fg"] @@ -42,24 +43,23 @@ class TestTheme(unittest.TestCase): Theme(self.invalidThemeName) def test_default_prefix(self): - self.assertEquals(self.theme.prefix(self.themedWidget), self.defaultPrefix) + self.assertEquals(self.theme.prefix(self.anyWidget), self.defaultPrefix) def test_default_suffix(self): - self.assertEquals(self.theme.suffix(self.themedWidget), self.defaultSuffix) + self.assertEquals(self.theme.suffix(self.anyWidget), self.defaultSuffix) def test_widget_prefix(self): - self.themedWidget.module = self.widgetTheme self.assertEquals(self.theme.prefix(self.themedWidget), self.widgetPrefix) def test_widget_fg(self): - self.assertEquals(self.theme.fg(self.themedWidget), self.defaultColor) - self.themedWidget.module = self.widgetTheme - self.assertEquals(self.theme.fg(self.themedWidget), self.widgetColor) + self.assertEquals(self.theme.fg(self.anyWidget), self.defaultColor) + self.anyWidget.module = self.widgetTheme + self.assertEquals(self.theme.fg(self.anyWidget), self.widgetColor) def test_widget_bg(self): - self.assertEquals(self.theme.bg(self.themedWidget), self.defaultBgColor) - self.themedWidget.module = self.widgetTheme - self.assertEquals(self.theme.bg(self.themedWidget), self.widgetBgColor) + self.assertEquals(self.theme.bg(self.anyWidget), self.defaultBgColor) + self.anyWidget.module = self.widgetTheme + self.assertEquals(self.theme.bg(self.anyWidget), self.widgetBgColor) def test_absent_cycle(self): theme = self.theme @@ -94,4 +94,21 @@ class TestTheme(unittest.TestCase): self.assertEquals(theme.separator_fg(self.anotherWidget), theme.bg(self.anotherWidget)) self.assertEquals(theme.separator_bg(self.anotherWidget), prev_bg) + def test_state(self): + theme = self.theme + data = theme.data() + + self.assertEquals(theme.fg(self.anyWidget), data["defaults"]["fg"]) + self.assertEquals(theme.bg(self.anyWidget), data["defaults"]["bg"]) + + self.anyWidget.attr_state = "critical" + self.assertEquals(theme.fg(self.anyWidget), data["defaults"]["critical"]["fg"]) + self.assertEquals(theme.bg(self.anyWidget), data["defaults"]["critical"]["bg"]) + + self.themedWidget.attr_state = "critical" + self.assertEquals(theme.fg(self.themedWidget), data[self.widgetTheme]["critical"]["fg"]) + # if elements are missing in the state theme, they are taken from the + # widget theme instead (i.e. no fallback to a more general state theme) + self.assertEquals(theme.bg(self.themedWidget), data[self.widgetTheme]["bg"]) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 314cfa0..ff9249d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -28,10 +28,14 @@ class MockOutput(object): def end(self): pass -class MockWidget(object): +class MockWidget(Widget): def __init__(self, text): self._text = text self.module = None + self.attr_state = "state-default" + + def state(self): + return self.attr_state def update(self, widgets): pass diff --git a/themes/test.json b/themes/test.json index 4640634..401a234 100644 --- a/themes/test.json +++ b/themes/test.json @@ -6,10 +6,17 @@ "fg": "#000000", "bg": "#111111", "separator": " * ", - "separator-block-width": 10 + "separator-block-width": 10, + "critical": { + "fg": "#ffffff", + "bg": "#010101" + } }, "test-widget": { "fg": "#ababab", - "bg": "#222222" + "bg": "#222222", + "critical": { + "fg": "#bababa" + } } } From fa30b9505bea4f0101844972e1eaafe89b15dd2f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 16:33:29 +0100 Subject: [PATCH 040/104] [all] More pylint fixes see #23 --- bumblebee/output.py | 1 + bumblebee/theme.py | 5 +++++ tests/test_theme.py | 5 +++-- tests/util.py | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bumblebee/output.py b/bumblebee/output.py index e582d34..6654cf7 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -19,6 +19,7 @@ class Widget(object): self.module = module.name def state(self): + """Return the widget's state""" return "state-default" def full_text(self): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index ddcd249..42f86ad 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -17,6 +17,9 @@ class Theme(object): def __init__(self, name): self._init(self.load(name)) self._widget = None + self._cycle_idx = 0 + self._cycle = {} + self._prevbg = None def _init(self, data): """Initialize theme from data structure""" @@ -67,9 +70,11 @@ class Theme(object): 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): diff --git a/tests/test_theme.py b/tests/test_theme.py index 75e7bc6..aa6ad6b 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -82,11 +82,12 @@ class TestTheme(unittest.TestCase): theme = self.theme data = theme.data() - self.assertEquals(theme.separator_block_width(self.anyWidget), data["defaults"]["separator-block-width"]) + self.assertEquals(theme.separator_block_width(self.anyWidget), + data["defaults"]["separator-block-width"] + ) def test_separator(self): for theme in [self.theme, self.cycleTheme]: - data = theme.data() theme.reset() prev_bg = theme.bg(self.anyWidget) theme.bg(self.anotherWidget) diff --git a/tests/util.py b/tests/util.py index ff9249d..fcf6c9d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -30,6 +30,7 @@ class MockOutput(object): class MockWidget(Widget): def __init__(self, text): + super(MockWidget, self).__init__(text) self._text = text self.module = None self.attr_state = "state-default" From e72c25b0bc4496ab822651534f5bb6224d98103b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 19:29:16 +0100 Subject: [PATCH 041/104] [core] Add input processing Create infrastructure for input event handling and add i3bar event processing. For each event, callbacks can be registered in the input module. Modules and widgets both identify themselves using a unique ID (the module name for modules, a generated UUID for the widgets). This ID is then used for registering the callbacks. This is possible since both widgets and modules are statically allocated & do not change their IDs. Callback actions can be either callable Python objects (in which case the event is passed as parameter), or strings, in which case the string is interpreted as a shell command. see #23 --- bumblebee-status | 19 ++++-- bumblebee/engine.py | 8 ++- bumblebee/input.py | 77 ++++++++++++++++++++++++ bumblebee/modules/cpu.py | 2 + bumblebee/output.py | 6 +- bumblebee/util.py | 21 +++++++ tests/test_engine.py | 4 +- tests/test_i3barinput.py | 120 ++++++++++++++++++++++++++++++++++++++ tests/test_i3baroutput.py | 16 ++--- tests/util.py | 20 ++++++- 10 files changed, 274 insertions(+), 19 deletions(-) create mode 100644 bumblebee/input.py create mode 100644 bumblebee/util.py create mode 100644 tests/test_i3barinput.py diff --git a/bumblebee-status b/bumblebee-status index 28e8e88..44eae21 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -5,23 +5,34 @@ import bumblebee.theme import bumblebee.engine import bumblebee.config import bumblebee.output +import bumblebee.input def main(): config = bumblebee.config.Config(sys.argv[1:]) theme = bumblebee.theme.Theme(config.theme()) output = bumblebee.output.I3BarOutput(theme=theme) + inp = bumblebee.input.I3BarInput() engine = bumblebee.engine.Engine( config=config, output=output, + inp=inp, ) - engine.run() - -if __name__ == "__main__": try: - main() + engine.run() + except KeyboardInterrupt as error: + inp.stop() + sys.exit(0) except bumblebee.error.BaseError as error: + inp.stop() sys.stderr.write("fatal: {}\n".format(error)) sys.exit(1) + except Exception as error: + inp.stop() + sys.stderr.write("fatal: {}\n".format(error)) + sys.exit(2) + +if __name__ == "__main__": + main() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 463d244..40cd152 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -30,6 +30,7 @@ class Module(object): self._config = config if "name" not in self._config: self._config["name"] = self.name + self.id = self._config["name"] self._widgets = [] if widgets: self._widgets = widgets if isinstance(widgets, list) else [widgets] @@ -53,12 +54,14 @@ class Engine(object): This class connects input/output, instantiates all required modules and drives the "event loop" """ - def __init__(self, config, output=None): + def __init__(self, config, output=None, inp=None): self._output = output self._config = config self._running = True self._modules = [] + self.input = inp self.load_modules(config.modules()) + self.input.start() def load_modules(self, modules): """Load specified modules and return them as list""" @@ -96,12 +99,13 @@ class Engine(object): module.update(module.widgets()) for widget in module.widgets(): widget.link_module(module) - self._output.draw(widget=widget, engine=self) + self._output.draw(widget=widget, module=module, engine=self) self._output.flush() self._output.end() if self.running(): time.sleep(1) self._output.stop() + self.input.stop() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/input.py b/bumblebee/input.py new file mode 100644 index 0000000..b28c237 --- /dev/null +++ b/bumblebee/input.py @@ -0,0 +1,77 @@ +"""Input classes""" + +import sys +import json +import uuid +import time +import threading +import bumblebee.util + +LEFT_MOUSE = 1 +RIGHT_MOUSE = 3 + +def read_input(inp): + """Read i3bar input and execute callbacks""" + while inp.running: + line = sys.stdin.readline().strip(",").strip() + inp.has_event = True + try: + event = json.loads(line) + inp.callback(event) + except ValueError: + pass + inp.has_event = True + inp.clean_exit = True + +class I3BarInput(object): + """Process incoming events from the i3bar""" + def __init__(self): + self.running = True + self._thread = threading.Thread(target=read_input, args=(self,)) + self._callbacks = {} + self.clean_exit = False + self.global_id = str(uuid.uuid4()) + self.need_event = False + self.has_event = False + + def start(self): + """Start asynchronous input processing""" + self._thread.start() + + def alive(self): + """Check whether the input processing is still active""" + return self._thread.is_alive() + + def stop(self): + """Stop asynchronous input processing""" + if self.need_event: + while not self.has_event: + time.sleep(0.1) + self.running = False + self._thread.join() + return self.clean_exit + + def register_callback(self, obj, button, cmd): + """Register a callback function or system call""" + uid = self.global_id + if obj: + uid = obj.id + + if uid not in self._callbacks: + self._callbacks[uid] = {} + self._callbacks[uid][button] = cmd + + def callback(self, event): + """Execute callback action for an incoming event""" + cmd = self._callbacks.get(self.global_id, {}) + cmd = self._callbacks.get(event["name"], cmd) + cmd = self._callbacks.get(event["instance"], cmd) + cmd = cmd.get(event["button"], None) + if cmd is None: + return + if callable(cmd): + cmd(event) + else: + bumblebee.util.execute(cmd) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index 2951ad8..b02b95b 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -11,6 +11,8 @@ class Module(bumblebee.engine.Module): bumblebee.output.Widget(full_text=self.utilization) ) self._utilization = psutil.cpu_percent(percpu=False) + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd="gnome-system-monitor") def utilization(self): return "{:05.02f}%".format(self._utilization) diff --git a/bumblebee/output.py b/bumblebee/output.py index 6654cf7..e67eff6 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -4,12 +4,14 @@ import sys import json +import uuid class Widget(object): """Represents a single visible block in the status bar""" def __init__(self, full_text): self._full_text = full_text self.module = None + self.id = str(uuid.uuid4()) def link_module(self, module): """Set the module that spawned this widget @@ -43,7 +45,7 @@ class I3BarOutput(object): """Finish i3bar protocol""" sys.stdout.write("]\n") - def draw(self, widget, engine=None): + def draw(self, widget, module=None, engine=None): """Draw a single widget""" full_text = widget.full_text() padding = self._theme.padding(widget) @@ -68,6 +70,8 @@ class I3BarOutput(object): "background": self._theme.bg(widget), "separator_block_width": self._theme.separator_block_width(widget), "separator": True if separator is None else False, + "instance": widget.id, + "name": module.id, }) def begin(self): diff --git a/bumblebee/util.py b/bumblebee/util.py new file mode 100644 index 0000000..9896c3f --- /dev/null +++ b/bumblebee/util.py @@ -0,0 +1,21 @@ +import shlex +import subprocess + +try: + from exceptions import RuntimeError +except ImportError: + # Python3 doesn't require this anymore + pass + +def execute(cmd, wait=True): + args = shlex.split(cmd) + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + + if wait: + out, _ = proc.communicate() + if proc.returncode != 0: + raise RuntimeError("{} exited with {}".format(cmd, proc.returncode)) + return out.decode("utf-8") + return None + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_engine.py b/tests/test_engine.py index f48feb4..069a71d 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -6,11 +6,11 @@ from bumblebee.error import ModuleLoadError from bumblebee.engine import Engine from bumblebee.config import Config -from tests.util import MockOutput +from tests.util import MockOutput, MockInput class TestEngine(unittest.TestCase): def setUp(self): - self.engine = Engine(config=Config(), output=MockOutput()) + self.engine = Engine(config=Config(), output=MockOutput(), inp=MockInput()) self.singleWidgetModule = [{"module": "test", "name": "a"}] self.testModule = "test" self.invalidModule = "no-such-module" diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py new file mode 100644 index 0000000..7841e1a --- /dev/null +++ b/tests/test_i3barinput.py @@ -0,0 +1,120 @@ +# pylint: disable=C0103,C0111 + +import unittest +import json +import subprocess +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from tests.util import MockWidget, MockModule + +class TestI3BarInput(unittest.TestCase): + def setUp(self): + self.inp = I3BarInput() + self.inp.need_event = True + self.anyModule = MockModule() + self.anyWidget = MockWidget("test") + self.anyModule.id = "test-module" + self._called = 0 + + def callback(self, event): + self._called += 1 + + @mock.patch("sys.stdin") + def test_basic_read_event(self, mock_input): + mock_input.readline.return_value = "" + self.inp.start() + self.inp.stop() + mock_input.readline.assert_any_call() + + @mock.patch("sys.stdin") + def test_ignore_invalid_data(self, mock_input): + mock_input.readline.return_value = "garbage" + self.inp.start() + self.assertEquals(self.inp.alive(), True) + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + + @mock.patch("sys.stdin") + def test_ignore_invalid_event(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": None, + "instance": None, + "button": None, + }) + self.inp.start() + self.assertEquals(self.inp.alive(), True) + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + + @mock.patch("sys.stdin") + def test_global_callback(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": "somename", + "instance": "someinstance", + "button": bumblebee.input.LEFT_MOUSE, + }) + self.inp.register_callback(None, button=1, cmd=self.callback) + self.inp.start() + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called > 0) + + @mock.patch("sys.stdin") + def test_global_callback_button_missmatch(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": "somename", + "instance": "someinstance", + "button": bumblebee.input.RIGHT_MOUSE, + }) + self.inp.register_callback(None, button=1, cmd=self.callback) + self.inp.start() + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called == 0) + + @mock.patch("sys.stdin") + def test_module_callback(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": self.anyModule.id, + "instance": None, + "button": bumblebee.input.LEFT_MOUSE, + }) + self.inp.register_callback(self.anyModule, button=1, cmd=self.callback) + self.inp.start() + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called > 0) + + @mock.patch("sys.stdin") + def test_widget_callback(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": "test", + "instance": self.anyWidget.id, + "button": bumblebee.input.LEFT_MOUSE, + }) + self.inp.register_callback(self.anyWidget, button=1, cmd=self.callback) + self.inp.start() + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called > 0) + + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_widget_cmd_callback(self, mock_input, mock_output): + mock_input.readline.return_value = json.dumps({ + "name": "test", + "instance": self.anyWidget.id, + "button": bumblebee.input.LEFT_MOUSE, + }) + self.inp.register_callback(self.anyWidget, button=1, cmd="echo") + self.inp.start() + self.assertEquals(self.inp.stop(), True) + mock_input.readline.assert_any_call() + mock_output.assert_called_with(["echo"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_i3baroutput.py b/tests/test_i3baroutput.py index 60ed671..f4c4cca 100644 --- a/tests/test_i3baroutput.py +++ b/tests/test_i3baroutput.py @@ -9,8 +9,7 @@ except ImportError: from io import StringIO from bumblebee.output import I3BarOutput -from tests.util import MockWidget -from tests.util import MockTheme +from tests.util import MockWidget, MockTheme, MockModule class TestI3BarOutput(unittest.TestCase): def setUp(self): @@ -19,6 +18,7 @@ class TestI3BarOutput(unittest.TestCase): self.expectedStart = json.dumps({"version": 1, "click_events": True}) + "[\n" self.expectedStop = "]\n" self.someWidget = MockWidget("foo bar baz") + self.anyModule = MockModule(None, None) self.anyColor = "#ababab" self.anotherColor = "#cccccc" @@ -34,7 +34,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_draw_single_widget(self, stdout): - self.output.draw(self.someWidget) + self.output.draw(self.someWidget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], self.someWidget.full_text()) @@ -42,7 +42,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_draw_multiple_widgets(self, stdout): for widget in [self.someWidget, self.someWidget]: - self.output.draw(widget) + self.output.draw(widget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue()) for res in result: @@ -61,7 +61,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_prefix(self, stdout): self.theme.attr_prefix = " - " - self.output.draw(self.someWidget) + self.output.draw(self.someWidget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( @@ -71,7 +71,7 @@ class TestI3BarOutput(unittest.TestCase): @mock.patch("sys.stdout", new_callable=StringIO) def test_suffix(self, stdout): self.theme.attr_suffix = " - " - self.output.draw(self.someWidget) + self.output.draw(self.someWidget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}".format( @@ -82,7 +82,7 @@ class TestI3BarOutput(unittest.TestCase): def test_bothfix(self, stdout): self.theme.attr_suffix = " - " self.theme.attr_prefix = " * " - self.output.draw(self.someWidget) + self.output.draw(self.someWidget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["full_text"], "{}{}{}".format( @@ -95,7 +95,7 @@ class TestI3BarOutput(unittest.TestCase): def test_colors(self, stdout): self.theme.attr_fg = self.anyColor self.theme.attr_bg = self.anotherColor - self.output.draw(self.someWidget) + self.output.draw(self.someWidget, self.anyModule) self.output.flush() result = json.loads(stdout.getvalue())[0] self.assertEquals(result["color"], self.anyColor) diff --git a/tests/util.py b/tests/util.py index fcf6c9d..1c244c8 100644 --- a/tests/util.py +++ b/tests/util.py @@ -6,8 +6,19 @@ def assertWidgetAttributes(test, widget): test.assertTrue(isinstance(widget, Widget)) test.assertTrue(hasattr(widget, "full_text")) +class MockInput(object): + def start(self): + pass + + def stop(self): + pass + + def register_callback(self, obj, button, cmd): + pass + class MockEngine(object): - pass + def __init__(self): + self.input = MockInput() class MockOutput(object): def start(self): @@ -16,7 +27,7 @@ class MockOutput(object): def stop(self): pass - def draw(self, widget, engine): + def draw(self, widget, engine, module): engine.stop() def begin(self): @@ -28,12 +39,17 @@ class MockOutput(object): def end(self): pass +class MockModule(object): + def __init__(self, engine=None, config=None): + self.id = None + class MockWidget(Widget): def __init__(self, text): super(MockWidget, self).__init__(text) self._text = text self.module = None self.attr_state = "state-default" + self.id = "none" def state(self): return self.attr_state From 9ce7739efbb339f7d955bb459d704bb9020ecd40 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Fri, 9 Dec 2016 22:28:04 +0100 Subject: [PATCH 042/104] [tests] Add specific tests for CPU module * Check that the left mouse button action works * Check that the format is OK see #23 --- tests/modules/test_cpu.py | 37 +++++++++++++++++++++++++ tests/test_i3barinput.py | 57 +++++++++++++++++++-------------------- tests/util.py | 10 +++++++ 3 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 tests/modules/test_cpu.py diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py new file mode 100644 index 0000000..4bce1e2 --- /dev/null +++ b/tests/modules/test_cpu.py @@ -0,0 +1,37 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.cpu import Module +from tests.util import MockEngine, assertPopen + +class TestCPUModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.module = Module(engine=self.engine, config={}) + + @mock.patch("sys.stdout") + def test_format(self, mock_output): + for widget in self.module.widgets(): + self.assertEquals(len(widget.full_text()), len("00.00%")) + + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_leftclick(self, mock_input, mock_output): + mock_input.readline.return_value = json.dumps({ + "name": self.module.id, + "button": bumblebee.input.LEFT_MOUSE, + "instance": None + }) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, "gnome-system-monitor") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index 7841e1a..08fa036 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -7,12 +7,12 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput -from tests.util import MockWidget, MockModule +from tests.util import MockWidget, MockModule, assertPopen class TestI3BarInput(unittest.TestCase): def setUp(self): - self.inp = I3BarInput() - self.inp.need_event = True + self.input = I3BarInput() + self.input.need_event = True self.anyModule = MockModule() self.anyWidget = MockWidget("test") self.anyModule.id = "test-module" @@ -24,16 +24,16 @@ class TestI3BarInput(unittest.TestCase): @mock.patch("sys.stdin") def test_basic_read_event(self, mock_input): mock_input.readline.return_value = "" - self.inp.start() - self.inp.stop() + self.input.start() + self.input.stop() mock_input.readline.assert_any_call() @mock.patch("sys.stdin") def test_ignore_invalid_data(self, mock_input): mock_input.readline.return_value = "garbage" - self.inp.start() - self.assertEquals(self.inp.alive(), True) - self.assertEquals(self.inp.stop(), True) + self.input.start() + self.assertEquals(self.input.alive(), True) + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() @mock.patch("sys.stdin") @@ -43,9 +43,9 @@ class TestI3BarInput(unittest.TestCase): "instance": None, "button": None, }) - self.inp.start() - self.assertEquals(self.inp.alive(), True) - self.assertEquals(self.inp.stop(), True) + self.input.start() + self.assertEquals(self.input.alive(), True) + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() @mock.patch("sys.stdin") @@ -55,9 +55,9 @@ class TestI3BarInput(unittest.TestCase): "instance": "someinstance", "button": bumblebee.input.LEFT_MOUSE, }) - self.inp.register_callback(None, button=1, cmd=self.callback) - self.inp.start() - self.assertEquals(self.inp.stop(), True) + self.input.register_callback(None, button=1, cmd=self.callback) + self.input.start() + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) @@ -68,9 +68,9 @@ class TestI3BarInput(unittest.TestCase): "instance": "someinstance", "button": bumblebee.input.RIGHT_MOUSE, }) - self.inp.register_callback(None, button=1, cmd=self.callback) - self.inp.start() - self.assertEquals(self.inp.stop(), True) + self.input.register_callback(None, button=1, cmd=self.callback) + self.input.start() + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) @@ -81,9 +81,9 @@ class TestI3BarInput(unittest.TestCase): "instance": None, "button": bumblebee.input.LEFT_MOUSE, }) - self.inp.register_callback(self.anyModule, button=1, cmd=self.callback) - self.inp.start() - self.assertEquals(self.inp.stop(), True) + self.input.register_callback(self.anyModule, button=1, cmd=self.callback) + self.input.start() + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) @@ -94,9 +94,9 @@ class TestI3BarInput(unittest.TestCase): "instance": self.anyWidget.id, "button": bumblebee.input.LEFT_MOUSE, }) - self.inp.register_callback(self.anyWidget, button=1, cmd=self.callback) - self.inp.start() - self.assertEquals(self.inp.stop(), True) + self.input.register_callback(self.anyWidget, button=1, cmd=self.callback) + self.input.start() + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) @@ -108,13 +108,10 @@ class TestI3BarInput(unittest.TestCase): "instance": self.anyWidget.id, "button": bumblebee.input.LEFT_MOUSE, }) - self.inp.register_callback(self.anyWidget, button=1, cmd="echo") - self.inp.start() - self.assertEquals(self.inp.stop(), True) + self.input.register_callback(self.anyWidget, button=1, cmd="echo") + self.input.start() + self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() - mock_output.assert_called_with(["echo"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) + assertPopen(mock_output, "echo") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 1c244c8..7881c99 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,11 +1,21 @@ # pylint: disable=C0103,C0111,W0613 +import shlex +import subprocess + from bumblebee.output import Widget def assertWidgetAttributes(test, widget): test.assertTrue(isinstance(widget, Widget)) test.assertTrue(hasattr(widget, "full_text")) +def assertPopen(output, cmd): + res = shlex.split(cmd) + output.assert_called_with(res, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + class MockInput(object): def start(self): pass From 87e76b9e40ec434c4df1172c2e18fa98f081a311 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 07:47:24 +0100 Subject: [PATCH 043/104] [modules/cmus] Re-add cmus module Re-add a first version of the cmus module originally contributed by @paxy97. Still missing: * Icon themes (status) * On-click actions see #23 --- bumblebee/modules/cmus.py | 56 ++++++++++++++++++++++++++++++++++++++ bumblebee/modules/cpu.py | 2 ++ bumblebee/output.py | 3 +- bumblebee/util.py | 8 ++++++ tests/modules/test_cmus.py | 30 ++++++++++++++++++++ tests/util.py | 4 +++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 bumblebee/modules/cmus.py create mode 100644 tests/modules/test_cmus.py diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py new file mode 100644 index 0000000..f2dfea3 --- /dev/null +++ b/bumblebee/modules/cmus.py @@ -0,0 +1,56 @@ +# pylint: disable=C0111,R0903 + +"""Displays information about the current song in cmus.""" + +from collections import defaultdict + +import string + +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="cmus.prev"), + bumblebee.output.Widget(name="cmus.main", full_text=self.description), + bumblebee.output.Widget(name="cmus.next"), + bumblebee.output.Widget(name="cmus.shuffle"), + bumblebee.output.Widget(name="cmus.repeat"), + ] + super(Module, self).__init__(engine, config, widgets) + self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}") + + def description(self): + return string.Formatter().vformat(self._fmt, (), self._tags) + + def update(self, widgets): + self._load_song() + + def _load_song(self): + info = "" + try: + info = bumblebee.util.execute("cmus-remote -Q") + except RuntimeError: + pass + self._tags = defaultdict(lambda: '') + for line in info.split("\n"): + if line.startswith("status"): + self._status = line.split(" ", 2)[1] + if line.startswith("tag"): + key, value = line.split(" ", 2)[1:] + self._tags.update({ key: value }) + if line.startswith("duration"): + self._tags.update({ + "duration": bumblebee.util.durationfmt(int(line.split(" ")[1])) + }) + if line.startswith("position"): + self._tags.update({ + "position": bumblebee.util.durationfmt(int(line.split(" ")[1])) + }) + if line.startswith("set repeat "): + self._repeat = False if "false" in line else True + if line.startswith("set shuffle "): + self._shuffle = False if "false" in line else True diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index b02b95b..3d20479 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -3,6 +3,8 @@ """Displays CPU utilization across all CPUs.""" import psutil +import bumblebee.input +import bumblebee.output import bumblebee.engine class Module(bumblebee.engine.Module): diff --git a/bumblebee/output.py b/bumblebee/output.py index e67eff6..30062f3 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -8,9 +8,10 @@ import uuid class Widget(object): """Represents a single visible block in the status bar""" - def __init__(self, full_text): + def __init__(self, full_text="", name=""): self._full_text = full_text self.module = None + self.name = name self.id = str(uuid.uuid4()) def link_module(self, module): diff --git a/bumblebee/util.py b/bumblebee/util.py index 9896c3f..24e2cb1 100644 --- a/bumblebee/util.py +++ b/bumblebee/util.py @@ -18,4 +18,12 @@ def execute(cmd, wait=True): return out.decode("utf-8") return None +def durationfmt(duration): + minutes, seconds = divmod(duration, 60) + hours, minutes = divmod(minutes, 60) + res = "{:02d}:{:02d}".format(minutes, seconds) + if hours > 0: res = "{:02d}:{}".format(hours, res) + + return res + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_cmus.py b/tests/modules/test_cmus.py new file mode 100644 index 0000000..dfb3c56 --- /dev/null +++ b/tests/modules/test_cmus.py @@ -0,0 +1,30 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.cmus import Module +from tests.util import MockEngine, MockConfig, assertPopen + +class TestCmusModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.module = Module(engine=self.engine, config={"config": MockConfig()}) + + @mock.patch("subprocess.Popen") + def test_read_song(self, mock_output): + rv = mock.Mock() + rv.configure_mock(**{ + "communicate.return_value": ("out", None) + }) + mock_output.return_value = rv + self.module.update(self.module.widgets()) + assertPopen(mock_output, "cmus-remote -Q") + + def test_widgets(self): + self.assertTrue(len(self.module.widgets()), 5) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 7881c99..6b04725 100644 --- a/tests/util.py +++ b/tests/util.py @@ -30,6 +30,10 @@ class MockEngine(object): def __init__(self): self.input = MockInput() +class MockConfig(object): + def get(self, name, default): + return default + class MockOutput(object): def start(self): pass From 225d471c6a4a7cc93b5049c9e633665e74054f50 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 08:09:13 +0100 Subject: [PATCH 044/104] [modules/cpu] Add configurable warning and critical thresholds The cpu module now has cpu.warning and cpu.critical thresholds. If the CPU utilization is higher than any of those values, the widget's state changes to warning or critical, respectively. see #23 --- bumblebee-status | 26 +++++++++++++------------- bumblebee/modules/cpu.py | 14 +++++++++++++- bumblebee/output.py | 6 +++++- tests/modules/test_cpu.py | 23 +++++++++++++++++++++-- tests/util.py | 8 ++++++++ 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/bumblebee-status b/bumblebee-status index 44eae21..248ff52 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -18,19 +18,19 @@ def main(): inp=inp, ) - try: - engine.run() - except KeyboardInterrupt as error: - inp.stop() - sys.exit(0) - except bumblebee.error.BaseError as error: - inp.stop() - sys.stderr.write("fatal: {}\n".format(error)) - sys.exit(1) - except Exception as error: - inp.stop() - sys.stderr.write("fatal: {}\n".format(error)) - sys.exit(2) + engine.run() +# try: +# except KeyboardInterrupt as error: +# inp.stop() +# sys.exit(0) +# except bumblebee.error.BaseError as error: +# inp.stop() +# sys.stderr.write("fatal: {}\n".format(error)) +# sys.exit(1) +# except Exception as error: +# inp.stop() +# sys.stderr.write("fatal: {}\n".format(error)) +# sys.exit(2) if __name__ == "__main__": main() diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index 3d20479..bf4cbc6 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -1,6 +1,11 @@ # pylint: disable=C0111,R0903 -"""Displays CPU utilization across all CPUs.""" +"""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%) +""" import psutil import bumblebee.input @@ -22,4 +27,11 @@ class Module(bumblebee.engine.Module): def update(self, widgets): self._utilization = psutil.cpu_percent(percpu=False) + def state(self, widget): + if self._utilization > int(self.parameter("critical", 80)): + return "critical" + if self._utilization > int(self.parameter("warning", 70)): + return "warning" + return None + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 30062f3..70bd864 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -11,6 +11,7 @@ class Widget(object): def __init__(self, full_text="", name=""): self._full_text = full_text self.module = None + self._module = None self.name = name self.id = str(uuid.uuid4()) @@ -20,10 +21,13 @@ class Widget(object): 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 state(self): """Return the widget's state""" - return "state-default" + if self._module and hasattr(self._module, "state"): + return self._module.state(self) + return None def full_text(self): """Retrieve the full text to display in the widget""" diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 4bce1e2..241656c 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -7,14 +7,17 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.cpu import Module -from tests.util import MockEngine, assertPopen +from tests.util import MockEngine, MockConfig, assertPopen class TestCPUModule(unittest.TestCase): def setUp(self): self.engine = MockEngine() self.engine.input = I3BarInput() self.engine.input.need_event = True - self.module = Module(engine=self.engine, config={}) + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + for widget in self.module.widgets(): + widget.link_module(self.module) @mock.patch("sys.stdout") def test_format(self, mock_output): @@ -34,4 +37,20 @@ class TestCPUModule(unittest.TestCase): mock_input.readline.assert_any_call() assertPopen(mock_output, "gnome-system-monitor") + @mock.patch("psutil.cpu_percent") + def test_warning(self, mock_psutil): + self.config.set("cpu.critical", "20") + self.config.set("cpu.warning", "18") + mock_psutil.return_value = 19.0 + self.module.update(self.module.widgets()) + self.assertEquals(self.module.widgets()[0].state(), "warning") + + @mock.patch("psutil.cpu_percent") + def test_critical(self, mock_psutil): + self.config.set("cpu.critical", "20") + self.config.set("cpu.warning", "19") + mock_psutil.return_value = 21.0 + self.module.update(self.module.widgets()) + self.assertEquals(self.module.widgets()[0].state(), "critical") + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 6b04725..f755a7a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -31,9 +31,17 @@ class MockEngine(object): self.input = MockInput() class MockConfig(object): + def __init__(self): + self._data = {} + def get(self, name, default): + if name in self._data: + return self._data[name] return default + def set(self, name, value): + self._data[name] = value + class MockOutput(object): def start(self): pass From 1a3217bb5fd27c8597c7eaab9681d68d9507fb9a Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 08:16:27 +0100 Subject: [PATCH 045/104] [modules/cmus] Add status callback Inform the theme about the current playback status (start, stop, repeat, shuffle). see #23 --- bumblebee/modules/cmus.py | 14 ++++++++++++++ themes/icons/awesome-fonts.json | 6 ++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index f2dfea3..b58d47f 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -22,6 +22,9 @@ class Module(bumblebee.engine.Module): ] super(Module, self).__init__(engine, config, widgets) self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}") + self._status = None + self._shuffle = False + self._repeat = False def description(self): return string.Formatter().vformat(self._fmt, (), self._tags) @@ -29,6 +32,17 @@ class Module(bumblebee.engine.Module): def update(self, widgets): self._load_song() + def state(self, widget): + if widget.name == "cmus.shuffle": + return "shuffle-on" if self._shuffle else "shuffle-off" + if widget.name == "cmus.repeat": + return "repeat-on" if self._repeat else "repeat-off" + if widget.name == "cmus.prev": + return "prev" + if widget.name == "cmus.next": + return "next" + return self._status + def _load_song(self): info = "" try: diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 3424710..c019337 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -13,8 +13,10 @@ "stopped": { "prefix": "" }, "prev": { "prefix": "" }, "next": { "prefix": "" }, - "shuffle": { "on": { "prefix": "" }, "off": { "prefix": "" } }, - "repeat": { "on": { "prefix": "" }, "off": { "prefix": "" } } + "shuffle-on": { "prefix": "" }, + "shuffle-off": { "prefix": "" }, + "repeat-on": { "prefix": "" }, + "repeat-off": { "prefix": "" } }, "pasink": { "muted": { "prefix": "" }, From b1ec41f9050411cd073b582baa2382345d70566d Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 08:19:24 +0100 Subject: [PATCH 046/104] [modules/cmus] Add mouse controls Enable play/pause, repeat/shuffle toggle, next/prev song by clicking on the various elements in the bar. see #23 --- bumblebee/modules/cmus.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index b58d47f..cdd52a8 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -21,6 +21,18 @@ class Module(bumblebee.engine.Module): bumblebee.output.Widget(name="cmus.repeat"), ] super(Module, self).__init__(engine, config, widgets) + + engine.input.register_callback(widgets[0], button=bumblebee.input.LEFT_MOUSE, + cmd="cmus-remote -r") + engine.input.register_callback(widgets[1], button=bumblebee.input.LEFT_MOUSE, + cmd="cmus-remote -u") + engine.input.register_callback(widgets[2], button=bumblebee.input.LEFT_MOUSE, + cmd="cmus-remote -n") + engine.input.register_callback(widgets[3], button=bumblebee.input.LEFT_MOUSE, + cmd="cmus-remote -S") + engine.input.register_callback(widgets[4], button=bumblebee.input.LEFT_MOUSE, + cmd="cmus-remote -R") + self._fmt = self.parameter("format", "{artist} - {title} {position}/{duration}") self._status = None self._shuffle = False From 38a42e4a77d77fb82e0a74e012ebfd83eb86b90b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 08:37:04 +0100 Subject: [PATCH 047/104] [tests/cmus] Add tests for cmus mouse interaction see #23 --- bumblebee/engine.py | 5 +++++ bumblebee/input.py | 12 +++++++++--- tests/modules/test_cmus.py | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 40cd152..caf43a7 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -39,6 +39,11 @@ class Module(object): """Return the widgets to draw for this module""" return self._widgets + def widget(self, name): + for widget in self._widgets: + if widget.name == name: + return widget + def update(self, widgets): """By default, update() is a NOP""" pass diff --git a/bumblebee/input.py b/bumblebee/input.py index b28c237..b9f36f5 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -27,7 +27,6 @@ class I3BarInput(object): """Process incoming events from the i3bar""" def __init__(self): self.running = True - self._thread = threading.Thread(target=read_input, args=(self,)) self._callbacks = {} self.clean_exit = False self.global_id = str(uuid.uuid4()) @@ -36,17 +35,24 @@ class I3BarInput(object): def start(self): """Start asynchronous input processing""" + self.has_event = False + self.running = True + self._thread = threading.Thread(target=read_input, args=(self,)) self._thread.start() def alive(self): """Check whether the input processing is still active""" return self._thread.is_alive() + def _wait(self): + while not self.has_event: + time.sleep(0.1) + self.has_event = False + def stop(self): """Stop asynchronous input processing""" if self.need_event: - while not self.has_event: - time.sleep(0.1) + self._wait() self.running = False self._thread.join() return self.clean_exit diff --git a/tests/modules/test_cmus.py b/tests/modules/test_cmus.py index dfb3c56..f31b65a 100644 --- a/tests/modules/test_cmus.py +++ b/tests/modules/test_cmus.py @@ -12,6 +12,8 @@ from tests.util import MockEngine, MockConfig, assertPopen class TestCmusModule(unittest.TestCase): def setUp(self): self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True self.module = Module(engine=self.engine, config={"config": MockConfig()}) @mock.patch("subprocess.Popen") @@ -27,4 +29,26 @@ class TestCmusModule(unittest.TestCase): def test_widgets(self): self.assertTrue(len(self.module.widgets()), 5) + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_interaction(self, mock_input, mock_output): + events = [ + {"widget": "cmus.shuffle", "action": "cmus-remote -S"}, + {"widget": "cmus.repeat", "action": "cmus-remote -R"}, + {"widget": "cmus.next", "action": "cmus-remote -n"}, + {"widget": "cmus.prev", "action": "cmus-remote -r"}, + {"widget": "cmus.main", "action": "cmus-remote -u"}, + ] + + for event in events: + mock_input.readline.return_value = json.dumps({ + "name": self.module.id, + "button": bumblebee.input.LEFT_MOUSE, + "instance": self.module.widget(event["widget"]).id + }) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, event["action"]) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 761b81970d80a45fedaf45742fbad9608133b38d Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 09:04:12 +0100 Subject: [PATCH 048/104] [modules/cpu] Pad to 3 digits before comma to fix width I cannot get the min_width property to work right now, so in order to fix the width of the CPU widget, pad the utilization to 3 digits (so that even 100% aligns nicely). see #23 --- bumblebee/modules/cpu.py | 2 +- tests/modules/test_cpu.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index bf4cbc6..214b205 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -22,7 +22,7 @@ class Module(bumblebee.engine.Module): cmd="gnome-system-monitor") def utilization(self): - return "{:05.02f}%".format(self._utilization) + return "{:06.02f}%".format(self._utilization) def update(self, widgets): self._utilization = psutil.cpu_percent(percpu=False) diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 241656c..e9f56cf 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -22,7 +22,7 @@ class TestCPUModule(unittest.TestCase): @mock.patch("sys.stdout") def test_format(self, mock_output): for widget in self.module.widgets(): - self.assertEquals(len(widget.full_text()), len("00.00%")) + self.assertEquals(len(widget.full_text()), len("100.00%")) @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") From 918d7a60466577b729a7b9be16dfde735a5feb0f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 10:26:07 +0100 Subject: [PATCH 049/104] [core/input] Add callback deregistration Enable components to unregister callbacks (i.e. for dynamic widgets). see #23 --- bumblebee/engine.py | 2 +- bumblebee/input.py | 24 ++++++++++++++++++++++-- tests/test_i3barinput.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index caf43a7..f0b6e03 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -108,7 +108,7 @@ class Engine(object): self._output.flush() self._output.end() if self.running(): - time.sleep(1) + self.input.wait(self._config.get("interval", 1)) self._output.stop() self.input.stop() diff --git a/bumblebee/input.py b/bumblebee/input.py index b9f36f5..e92da99 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -18,6 +18,7 @@ def read_input(inp): try: event = json.loads(line) inp.callback(event) + inp.redraw() except ValueError: pass inp.has_event = True @@ -32,18 +33,28 @@ class I3BarInput(object): 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) @@ -51,18 +62,27 @@ class I3BarInput(object): 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 register_callback(self, obj, button, cmd): - """Register a callback function or system call""" + def _uid(self, obj): uid = self.global_id if obj: uid = obj.id + return uid + def deregister_callbacks(self, obj): + uid = self._uid(obj) + if uid in self._callbacks: + del self._callbacks[uid] + + def register_callback(self, obj, button, cmd): + """Register a callback function or system call""" + uid = self._uid(obj) if uid not in self._callbacks: self._callbacks[uid] = {} self._callbacks[uid][button] = cmd diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index 08fa036..1ae2864 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -61,6 +61,20 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("sys.stdin") + def test_remove_global_callback(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": "somename", + "instance": "someinstance", + "button": bumblebee.input.LEFT_MOUSE, + }) + self.input.register_callback(None, button=1, cmd=self.callback) + self.input.deregister_callbacks(None) + self.input.start() + self.assertEquals(self.input.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called == 0) + @mock.patch("sys.stdin") def test_global_callback_button_missmatch(self, mock_input): mock_input.readline.return_value = json.dumps({ @@ -87,6 +101,20 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("sys.stdin") + def test_remove_module_callback(self, mock_input): + mock_input.readline.return_value = json.dumps({ + "name": self.anyModule.id, + "instance": None, + "button": bumblebee.input.LEFT_MOUSE, + }) + self.input.register_callback(self.anyModule, button=1, cmd=self.callback) + self.input.deregister_callbacks(self.anyModule) + self.input.start() + self.assertEquals(self.input.stop(), True) + mock_input.readline.assert_any_call() + self.assertTrue(self._called == 0) + @mock.patch("sys.stdin") def test_widget_callback(self, mock_input): mock_input.readline.return_value = json.dumps({ From c820223d0c525dd009a51314785ab1fa3b2bd3ac Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 10:47:23 +0100 Subject: [PATCH 050/104] [core/input] Execute commands in background When spawning a command from an input interaction, do it in the background, so as to not block further interactions. see #23 --- bumblebee/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index e92da99..75eaec6 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -98,6 +98,6 @@ class I3BarInput(object): if callable(cmd): cmd(event) else: - bumblebee.util.execute(cmd) + bumblebee.util.execute(cmd, False) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From a045962d0068067bda1fbab8900fde2a5845dc36 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 11:25:02 +0100 Subject: [PATCH 051/104] [modules/nic] Re-enable NIC module Re-add the NIC module with all its functionality (hopefully...). This introduces a new concept: Instead of having separate queries for critical and warning (which really are just another set of states), a module can now return a list of states for each widget. All the state information is then merged together into a single theme. So, for instance, the NIC module can return a state saying "critical - wlan-down", which applies the theme information for both "critical" and "wlan-down". see #23 --- bumblebee/modules/cmus.py | 2 ++ bumblebee/modules/nic.py | 67 +++++++++++++++++++++++++++++++++++ bumblebee/output.py | 25 ++++++++----- bumblebee/theme.py | 15 ++++---- tests/modules/test_cpu.py | 4 +-- tests/modules/test_modules.py | 4 +++ tests/test_theme.py | 4 +-- tests/util.py | 2 +- 8 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 bumblebee/modules/nic.py diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index cdd52a8..488de5a 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -80,3 +80,5 @@ class Module(bumblebee.engine.Module): self._repeat = False if "false" in line else True if line.startswith("set shuffle "): self._shuffle = False if "false" in line else True + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py new file mode 100644 index 0000000..272bd25 --- /dev/null +++ b/bumblebee/modules/nic.py @@ -0,0 +1,67 @@ +#pylint: disable=C0111,R0903 + +import netifaces + +import bumblebee.util +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +"""Displays the name, IP address(es) and status of each available network interface.""" + +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._update_widgets(widgets) + + 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") + + def _update_widgets(self, widgets): + interfaces = [ i for i in netifaces.interfaces() if not i.startswith(self._exclude) ] + for intf in interfaces: + addr = [] + state = "down" + try: + if netifaces.AF_INET in netifaces.ifaddresses(intf): + for ip in netifaces.ifaddresses(intf)[netifaces.AF_INET]: + if "addr" in ip and ip["addr"] != "": + addr.append(ip["addr"]) + state = "up" + except Exception as e: + addr = [] + widget = self.widget(intf) + if not widget: + widget = bumblebee.output.Widget(name=intf) + widgets.append(widget) + widget.full_text("{} {} {}".format(intf, state, ", ".join(addr))) + widget.set("intf", intf) + widget.set("state", state) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 70bd864..7e76cc2 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -6,9 +6,12 @@ import sys import json import uuid -class Widget(object): +import bumblebee.store + +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 @@ -26,15 +29,21 @@ class Widget(object): def state(self): """Return the widget's state""" if self._module and hasattr(self._module, "state"): - return self._module.state(self) - return None + states = self._module.state(self) + if not isinstance(states, list): + return [states] + return states + return [] - def full_text(self): - """Retrieve the full text to display in the widget""" - if callable(self._full_text): - return self._full_text() + def full_text(self, value=None): + """Set or retrieve the full text to display in the widget""" + if value: + self._full_text = value else: - return self._full_text + if callable(self._full_text): + return self._full_text() + else: + return self._full_text class I3BarOutput(object): """Manage output according to the i3bar protocol""" diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 42f86ad..1c56ffe 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -118,16 +118,19 @@ class Theme(object): self._cycle = self._cycles[self._cycle_idx] module_theme = self._theme.get(widget.module, {}) - if name != widget.state(): - # avoid infinite recursion - state_theme = self._get(widget, widget.state(), {}) - else: - state_theme = {} + + state_themes = [] + # avoid infinite recursion + if name not in widget.state(): + for state in widget.state(): + state_themes.append(self._get(widget, state, {})) value = self._defaults.get(name, default) value = self._cycle.get(name, value) value = module_theme.get(name, value) - value = state_theme.get(name, value) + + for theme in state_themes: + value = theme.get(name, value) return value diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index e9f56cf..6793b83 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -43,7 +43,7 @@ class TestCPUModule(unittest.TestCase): self.config.set("cpu.warning", "18") mock_psutil.return_value = 19.0 self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), "warning") + self.assertEquals(self.module.widgets()[0].state(), ["warning"]) @mock.patch("psutil.cpu_percent") def test_critical(self, mock_psutil): @@ -51,6 +51,6 @@ class TestCPUModule(unittest.TestCase): self.config.set("cpu.warning", "19") mock_psutil.return_value = 21.0 self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), "critical") + self.assertEquals(self.module.widgets()[0].state(), ["critical"]) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 60c01e2..0e01873 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -15,6 +15,8 @@ class TestGenericModules(unittest.TestCase): for mod in all_modules(): cls = importlib.import_module("bumblebee.modules.{}".format(mod["name"])) self.objects[mod["name"]] = getattr(cls, "Module")(engine, {"config": config}) + for widget in self.objects[mod["name"]].widgets(): + self.assertEquals(widget.get("variable", None), None) def test_widgets(self): for mod in self.objects: @@ -23,6 +25,8 @@ class TestGenericModules(unittest.TestCase): widget.link_module(self.objects[mod]) self.assertEquals(widget.module, mod) assertWidgetAttributes(self, widget) + widget.set("variable", "value") + self.assertEquals(widget.get("variable", None), "value") def test_update(self): for mod in self.objects: diff --git a/tests/test_theme.py b/tests/test_theme.py index aa6ad6b..8d9dccc 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -102,11 +102,11 @@ class TestTheme(unittest.TestCase): self.assertEquals(theme.fg(self.anyWidget), data["defaults"]["fg"]) self.assertEquals(theme.bg(self.anyWidget), data["defaults"]["bg"]) - self.anyWidget.attr_state = "critical" + self.anyWidget.attr_state = ["critical"] self.assertEquals(theme.fg(self.anyWidget), data["defaults"]["critical"]["fg"]) self.assertEquals(theme.bg(self.anyWidget), data["defaults"]["critical"]["bg"]) - self.themedWidget.attr_state = "critical" + self.themedWidget.attr_state = ["critical"] self.assertEquals(theme.fg(self.themedWidget), data[self.widgetTheme]["critical"]["fg"]) # if elements are missing in the state theme, they are taken from the # widget theme instead (i.e. no fallback to a more general state theme) diff --git a/tests/util.py b/tests/util.py index f755a7a..fc6bd14 100644 --- a/tests/util.py +++ b/tests/util.py @@ -70,7 +70,7 @@ class MockWidget(Widget): super(MockWidget, self).__init__(text) self._text = text self.module = None - self.attr_state = "state-default" + self.attr_state = ["state-default"] self.id = "none" def state(self): From 72e375ac8bd5c82008ecd3fe15c97a20646ff8a2 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 11:33:35 +0100 Subject: [PATCH 052/104] [modules/nic] Check for vanished interfaces If a widget exists for an interface that is not there anymore (i.e. a tunnel interface that has been removed, or a USB device that has been unplugged), remove that widget from the list. see #23 --- bumblebee/modules/nic.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py index 272bd25..2459be7 100644 --- a/bumblebee/modules/nic.py +++ b/bumblebee/modules/nic.py @@ -45,6 +45,10 @@ class Module(bumblebee.engine.Module): def _update_widgets(self, widgets): interfaces = [ i for i in netifaces.interfaces() if not i.startswith(self._exclude) ] + + for widget in widgets: + widget.set("visited", False) + for intf in interfaces: addr = [] state = "down" @@ -63,5 +67,10 @@ class Module(bumblebee.engine.Module): widget.full_text("{} {} {}".format(intf, state, ", ".join(addr))) widget.set("intf", intf) widget.set("state", state) + widget.set("visited", True) + + for widget in widgets: + if widget.get("visited") == False: + widgets.remove(widget) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 4d4a7bf29d553448acb736045132478465838149 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 12:00:08 +0100 Subject: [PATCH 053/104] [modules/battery] Re-enable battery module Extend theme to be able to accept lists of values and cycle through them. Use this to "animate" the charging symbol for the battery. see #23 --- bumblebee/modules/battery.py | 54 ++++++++++++++++++++++++++++++++++++ bumblebee/theme.py | 6 ++++ 2 files changed, 60 insertions(+) create mode 100644 bumblebee/modules/battery.py diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py new file mode 100644 index 0000000..ee58d27 --- /dev/null +++ b/bumblebee/modules/battery.py @@ -0,0 +1,54 @@ +# pylint: disable=C0111,R0903 + +import os + +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.capacity) + ) + battery = self.parameter("device", "BAT0") + self._path = "/sys/class/power_supply/{}".format(battery) + self._capacity = 100 + + def capacity(self): + return "{:02d}%".format(self._capacity) + + def update(self, widgets): + widget = widgets[0] + self._ac = False + if not os.path.exists(self._path): + self._ac = True + + with open(self._path + "/capacity") as f: + self._capacity = int(f.read()) + self._capacity = self._capacity if self._capacity < 100 else 100 + + def state(self, widget): + state = [] + if self._capacity < self.parameter("critical", 10): + state.append("critical") + elif self._capacity < self.parameter("warning", 20): + state.append("warning") + + if self._ac: + state.append("AC") + else: + charge = "" + with open(self._path + "/status") as f: + charge = f.read().strip() + if charge == "Discharging": + state.append("discharging-{}".format(min([10, 25, 50, 80, 100] , key=lambda i:abs(i-self._capacity)))) + else: + if self._capacity > 95: + state.append("charged") + else: + state.append("charging") + + return state + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/theme.py b/bumblebee/theme.py index 1c56ffe..a7a7ccd 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -132,6 +132,12 @@ class Theme(object): 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] + return value # algorithm copied from From 7ea8c5320d57e2fb8c9e59dfa9da487526ca2731 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 12:03:58 +0100 Subject: [PATCH 054/104] [modules] Add help texts see #23 --- bumblebee/modules/battery.py | 8 ++++++++ bumblebee/modules/cmus.py | 6 +++++- bumblebee/modules/nic.py | 8 ++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index ee58d27..2e945de 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -1,5 +1,13 @@ # pylint: disable=C0111,R0903 +"""Displays battery status, remaining percentage and charging information. + +Parameters: + * battery.device : The device to read information from (defaults to BAT0) + * battery.warning : Warning threshold in % of remaining charge (defaults to 20) + * battery.critical: Critical threshold in % of remaining charge (defaults to 10) +""" + import os import bumblebee.input diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index 488de5a..0505294 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -1,6 +1,10 @@ # pylint: disable=C0111,R0903 -"""Displays information about the current song in cmus.""" +"""Displays information about the current song in cmus. + +Parameters: + * cmus.format: Format string for the song information. Tag values can be put in curly brackets (i.e. {artist}) +""" from collections import defaultdict diff --git a/bumblebee/modules/nic.py b/bumblebee/modules/nic.py index 2459be7..bd94f94 100644 --- a/bumblebee/modules/nic.py +++ b/bumblebee/modules/nic.py @@ -1,5 +1,11 @@ #pylint: disable=C0111,R0903 +"""Displays the name, IP address(es) and status of each available network interface. + +Parameters: + * nic.exclude: Comma-separated list of interface prefixes to exclude (defaults to "lo,virbr,docker,vboxnet,veth") +""" + import netifaces import bumblebee.util @@ -7,8 +13,6 @@ import bumblebee.input import bumblebee.output import bumblebee.engine -"""Displays the name, IP address(es) and status of each available network interface.""" - class Module(bumblebee.engine.Module): def __init__(self, engine, config): widgets = [] From 0489ce1b518583960111d4886e071604fcf09385 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 12:14:12 +0100 Subject: [PATCH 055/104] [core/engine] Register wheel up/down callbacks for desktop switch Switch desktop to prev/next on wheel up/down. see #23 --- bumblebee/engine.py | 6 ++++++ bumblebee/input.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index f0b6e03..5c8d848 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -66,6 +66,12 @@ class Engine(object): self._modules = [] self.input = inp self.load_modules(config.modules()) + + 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") + self.input.start() def load_modules(self, modules): diff --git a/bumblebee/input.py b/bumblebee/input.py index 75eaec6..76b9bf6 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -9,6 +9,8 @@ import bumblebee.util LEFT_MOUSE = 1 RIGHT_MOUSE = 3 +WHEEL_UP = 4 +WHEEL_DOWN = 5 def read_input(inp): """Read i3bar input and execute callbacks""" From 029492e16d9487e0c274f00092bce1c36b673add Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 13:45:54 +0100 Subject: [PATCH 056/104] [core] Non-blocking input thread for i3bar events Make input thread non-blocking by using select(). This increases the CPU utilization a bit (depending on the timeout), but makes the thread exit cleanly, even if an exception is thrown in the main thread. see #23 --- bumblebee/input.py | 8 ++++++++ tests/modules/test_cmus.py | 5 ++++- tests/modules/test_cpu.py | 4 +++- tests/test_i3barinput.py | 40 ++++++++++++++++++++++++++++---------- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index 76b9bf6..a501a85 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -4,6 +4,7 @@ import sys import json import uuid import time +import select import threading import bumblebee.util @@ -15,6 +16,13 @@ WHEEL_DOWN = 5 def read_input(inp): """Read i3bar input and execute callbacks""" while inp.running: + for thread in threading.enumerate(): + if thread.name == "MainThread" and not thread.is_alive(): + return + + rlist, _, _ = select.select([sys.stdin], [], [], 1) + if not rlist: + continue line = sys.stdin.readline().strip(",").strip() inp.has_event = True try: diff --git a/tests/modules/test_cmus.py b/tests/modules/test_cmus.py index f31b65a..1bc8b67 100644 --- a/tests/modules/test_cmus.py +++ b/tests/modules/test_cmus.py @@ -29,9 +29,10 @@ class TestCmusModule(unittest.TestCase): def test_widgets(self): self.assertTrue(len(self.module.widgets()), 5) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") - def test_interaction(self, mock_input, mock_output): + def test_interaction(self, mock_input, mock_output, mock_select): events = [ {"widget": "cmus.shuffle", "action": "cmus-remote -S"}, {"widget": "cmus.repeat", "action": "cmus-remote -R"}, @@ -40,6 +41,8 @@ class TestCmusModule(unittest.TestCase): {"widget": "cmus.main", "action": "cmus-remote -u"}, ] + mock_select.return_value = (1,2,3) + for event in events: mock_input.readline.return_value = json.dumps({ "name": self.module.id, diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 6793b83..52b7967 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -24,14 +24,16 @@ class TestCPUModule(unittest.TestCase): for widget in self.module.widgets(): self.assertEquals(len(widget.full_text()), len("100.00%")) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") - def test_leftclick(self, mock_input, mock_output): + def test_leftclick(self, mock_input, mock_output, mock_select): mock_input.readline.return_value = json.dumps({ "name": self.module.id, "button": bumblebee.input.LEFT_MOUSE, "instance": None }) + mock_select.return_value = (1,2,3) self.engine.input.start() self.engine.input.stop() mock_input.readline.assert_any_call() diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index 1ae2864..57aa161 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -21,23 +21,29 @@ class TestI3BarInput(unittest.TestCase): def callback(self, event): self._called += 1 + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_basic_read_event(self, mock_input): + def test_basic_read_event(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = "" self.input.start() self.input.stop() mock_input.readline.assert_any_call() + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_ignore_invalid_data(self, mock_input): + def test_ignore_invalid_data(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = "garbage" self.input.start() self.assertEquals(self.input.alive(), True) self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_ignore_invalid_event(self, mock_input): + def test_ignore_invalid_event(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": None, "instance": None, @@ -48,8 +54,10 @@ class TestI3BarInput(unittest.TestCase): self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_global_callback(self, mock_input): + def test_global_callback(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": "somename", "instance": "someinstance", @@ -61,8 +69,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_remove_global_callback(self, mock_input): + def test_remove_global_callback(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": "somename", "instance": "someinstance", @@ -75,8 +85,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_global_callback_button_missmatch(self, mock_input): + def test_global_callback_button_missmatch(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": "somename", "instance": "someinstance", @@ -88,8 +100,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_module_callback(self, mock_input): + def test_module_callback(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": self.anyModule.id, "instance": None, @@ -101,8 +115,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_remove_module_callback(self, mock_input): + def test_remove_module_callback(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": self.anyModule.id, "instance": None, @@ -115,8 +131,10 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called == 0) + @mock.patch("select.select") @mock.patch("sys.stdin") - def test_widget_callback(self, mock_input): + def test_widget_callback(self, mock_input, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": "test", "instance": self.anyWidget.id, @@ -128,9 +146,11 @@ class TestI3BarInput(unittest.TestCase): mock_input.readline.assert_any_call() self.assertTrue(self._called > 0) + @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") - def test_widget_cmd_callback(self, mock_input, mock_output): + def test_widget_cmd_callback(self, mock_input, mock_output, mock_select): + mock_select.return_value = (1,2,3) mock_input.readline.return_value = json.dumps({ "name": "test", "instance": self.anyWidget.id, From 1b8385b33fa45fb1e0a4fe606c4e65af67c6f48e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 14:31:18 +0100 Subject: [PATCH 057/104] [modules/brighness] Re-enable brightness module Add a module for reporting, increasing and decreasing the brightness of a display. see #23 --- bumblebee/modules/brightness.py | 33 +++++++++++++++ tests/modules/test_brightness.py | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 bumblebee/modules/brightness.py create mode 100644 tests/modules/test_brightness.py diff --git a/bumblebee/modules/brightness.py b/bumblebee/modules/brightness.py new file mode 100644 index 0000000..56b5314 --- /dev/null +++ b/bumblebee/modules/brightness.py @@ -0,0 +1,33 @@ +# pylint: disable=C0111,R0903 + +"""Displays the brightness of a display + +Parameters: + * brightness.step: The amount of increase/decrease on scroll in % (defaults to 2) +""" + +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.brightness) + ) + self._brightness = 0 + + step = self.parameter("step", 2) + + engine.input.register_callback(self, button=bumblebee.input.WHEEL_UP, + cmd="xbacklight +{}%".format(step)) + engine.input.register_callback(self, button=bumblebee.input.WHEEL_DOWN, + cmd="xbacklight -{}%".format(step)) + + def brightness(self): + return "{:03.0f}%".format(self._brightness) + + def update(self, widgets): + self._brightness = float(bumblebee.util.execute("xbacklight -get")) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_brightness.py b/tests/modules/test_brightness.py new file mode 100644 index 0000000..9b0c105 --- /dev/null +++ b/tests/modules/test_brightness.py @@ -0,0 +1,72 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.brightness import Module +from tests.util import MockEngine, MockConfig, assertPopen + +class TestBrightnessModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + for widget in self.module.widgets(): + widget.link_module(self.module) + + @mock.patch("sys.stdout") + def test_format(self, mock_output): + for widget in self.module.widgets(): + self.assertEquals(len(widget.full_text()), len("100%")) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_wheel_up(self, mock_input, mock_output, mock_select): + mock_input.readline.return_value = json.dumps({ + "name": self.module.id, + "button": bumblebee.input.WHEEL_UP, + "instance": None + }) + mock_select.return_value = (1,2,3) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, "xbacklight +2%") + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_wheel_down(self, mock_input, mock_output, mock_select): + mock_input.readline.return_value = json.dumps({ + "name": self.module.id, + "button": bumblebee.input.WHEEL_DOWN, + "instance": None + }) + mock_select.return_value = (1,2,3) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, "xbacklight -2%") + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_custom_step(self, mock_input, mock_output, mock_select): + self.config.set("brightness.step", "10") + module = Module(engine=self.engine, config={ "config": self.config }) + mock_input.readline.return_value = json.dumps({ + "name": module.id, + "button": bumblebee.input.WHEEL_DOWN, + "instance": None + }) + mock_select.return_value = (1,2,3) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, "xbacklight -10%") From 163419063d3d709269e458453bb2b83feb4afb86 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 14:50:49 +0100 Subject: [PATCH 058/104] [tests/battery] Add some tests for the battery module see #23 --- bumblebee/modules/battery.py | 6 ++-- tests/modules/test_battery.py | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 tests/modules/test_battery.py diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index 2e945de..9fa4c6e 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -24,7 +24,7 @@ class Module(bumblebee.engine.Module): self._capacity = 100 def capacity(self): - return "{:02d}%".format(self._capacity) + return "{:03d}%".format(self._capacity) def update(self, widgets): widget = widgets[0] @@ -38,9 +38,9 @@ class Module(bumblebee.engine.Module): def state(self, widget): state = [] - if self._capacity < self.parameter("critical", 10): + if self._capacity < int(self.parameter("critical", 10)): state.append("critical") - elif self._capacity < self.parameter("warning", 20): + elif self._capacity < int(self.parameter("warning", 20)): state.append("warning") if self._ac: diff --git a/tests/modules/test_battery.py b/tests/modules/test_battery.py new file mode 100644 index 0000000..da2e3cc --- /dev/null +++ b/tests/modules/test_battery.py @@ -0,0 +1,64 @@ +# pylint: disable=C0103,C0111 + +import sys +import json +import unittest +import mock + +from contextlib import contextmanager + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.battery import Module +from tests.util import MockEngine, MockConfig, assertPopen + +class MockOpen(object): + def __init__(self): + self._value = "" + + def returns(self, value): + self._value = value + + def __enter__(self): + return self + + def __exit__(self, a, b, c): + pass + + def read(self): + return self._value + +class TestBatteryModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + for widget in self.module.widgets(): + widget.link_module(self.module) + + @mock.patch("sys.stdout") + def test_format(self, mock_output): + for widget in self.module.widgets(): + self.assertEquals(len(widget.full_text()), len("100%")) + + @mock.patch("{}.open".format("__builtin__" if sys.version_info[0] < 3 else "builtins")) + @mock.patch("subprocess.Popen") + def test_critical(self, mock_output, mock_open): + mock_open.return_value = MockOpen() + mock_open.return_value.returns("19") + self.config.set("battery.critical", "20") + self.config.set("battery.warning", "25") + self.module.update(self.module.widgets()) + self.assertTrue("critical" in self.module.widgets()[0].state()) + + @mock.patch("{}.open".format("__builtin__" if sys.version_info[0] < 3 else "builtins")) + @mock.patch("subprocess.Popen") + def test_warning(self, mock_output, mock_open): + mock_open.return_value = MockOpen() + mock_open.return_value.returns("22") + self.config.set("battery.critical", "20") + self.config.set("battery.warning", "25") + self.module.update(self.module.widgets()) + self.assertTrue("warning" in self.module.widgets()[0].state()) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 771c597ce979dec6bad515931300dd5e1abce7ce Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 15:36:18 +0100 Subject: [PATCH 059/104] [modules/caffeine] Re-implement caffeine module Add caffeine module & add a framework for testing it (no tests yet, though). see #23 --- bumblebee/input.py | 13 ++++++++-- bumblebee/modules/caffeine.py | 47 ++++++++++++++++++++++++++++++++++ tests/modules/test_caffeine.py | 21 +++++++++++++++ tests/util.py | 2 +- 4 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 bumblebee/modules/caffeine.py create mode 100644 tests/modules/test_caffeine.py diff --git a/bumblebee/input.py b/bumblebee/input.py index a501a85..6876031 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -28,10 +28,12 @@ def read_input(inp): try: event = json.loads(line) inp.callback(event) + inp.has_valid_event = True inp.redraw() except ValueError: pass inp.has_event = True + inp.has_valid_event = True inp.clean_exit = True class I3BarInput(object): @@ -42,12 +44,15 @@ class I3BarInput(object): self.clean_exit = False self.global_id = str(uuid.uuid4()) self.need_event = False + self.need_valid_event = False self.has_event = False + self.has_valid_event = False self._condition = threading.Condition() def start(self): """Start asynchronous input processing""" self.has_event = False + self.has_valid_event = False self.running = True self._condition.acquire() self._thread = threading.Thread(target=read_input, args=(self,)) @@ -65,16 +70,20 @@ class I3BarInput(object): def wait(self, timeout): self._condition.wait(timeout) - def _wait(self): + def _wait(self, valid=False): while not self.has_event: time.sleep(0.1) + if valid: + while not self.has_valid_event: + time.sleep(0.1) self.has_event = False + self.has_valid_event = False def stop(self): """Stop asynchronous input processing""" self._condition.release() if self.need_event: - self._wait() + self._wait(self.need_valid_event) self.running = False self._thread.join() return self.clean_exit diff --git a/bumblebee/modules/caffeine.py b/bumblebee/modules/caffeine.py new file mode 100644 index 0000000..cc2754e --- /dev/null +++ b/bumblebee/modules/caffeine.py @@ -0,0 +1,47 @@ +# pylint: disable=C0111,R0903 + +"""Enable/disable automatic screen locking. +""" + +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.caffeine) + ) + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd=self._toggle + ) + + def caffeine(self): + return "" + + def state(self, widget): + if self._active(): + return "activated" + return "deactivated" + + def _active(self): + for line in bumblebee.util.execute("xset q").split("\n"): + if "timeout" in line: + timeout = int(line.split(" ")[4]) + if timeout == 0: + return True + return False + return False + + def update(self, widgets): + pass + + def _toggle(self, widget): + if self._active(): + bumblebee.util.execute("xset s default") + bumblebee.util.execute("notify-send \"Out of coffee\"") + else: + bumblebee.util.execute("xset s off") + bumblebee.util.execute("notify-send \"Consuming caffeine\"") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_caffeine.py b/tests/modules/test_caffeine.py new file mode 100644 index 0000000..995eece --- /dev/null +++ b/tests/modules/test_caffeine.py @@ -0,0 +1,21 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.caffeine import Module +from tests.util import MockEngine, MockConfig, assertPopen + +class TestCaffeineModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.engine.input.need_valid_event = True + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + for widget in self.module.widgets(): + widget.link_module(self.module) diff --git a/tests/util.py b/tests/util.py index fc6bd14..fded9f2 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,7 +11,7 @@ def assertWidgetAttributes(test, widget): def assertPopen(output, cmd): res = shlex.split(cmd) - output.assert_called_with(res, + output.assert_any_call(res, stdout=subprocess.PIPE, stderr=subprocess.STDOUT ) From 12f5ce5977c881069bb7939c39b2875f619a7d56 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 18:21:01 +0100 Subject: [PATCH 060/104] [modules/disk] Re-enable disk usage module Add a module that shows the disk usage for various paths and opens nautilus on that path whenever it is clicked. see #23 --- bumblebee/modules/disk.py | 50 ++++++++++++++++++++++++++++++++++++++ bumblebee/util.py | 7 ++++++ tests/modules/test_disk.py | 36 +++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 bumblebee/modules/disk.py create mode 100644 tests/modules/test_disk.py diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py new file mode 100644 index 0000000..586909f --- /dev/null +++ b/bumblebee/modules/disk.py @@ -0,0 +1,50 @@ +# 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 /) +""" + +import os + +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.diskspace) + ) + self._path = self.parameter("path", "/") + self._perc = 0 + + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd="nautilus {}".format(self._path)) + + def diskspace(self): + st = os.statvfs(self._path) + size = st.f_frsize*st.f_blocks + used = size - st.f_frsize*st.f_bavail + self._perc = 100.0*used/size + + return "{} {}/{} ({:05.02f}%)".format(self._path, + bumblebee.util.bytefmt(used), + bumblebee.util.bytefmt(size), self._perc + ) + + def update(self, widgets): + pass + + def state(self, widget): + pass + def warning(self, widget): + return self._perc > self._config.parameter("warning", 80) + + def critical(self, widget): + return self._perc > self._config.parameter("critical", 90) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/util.py b/bumblebee/util.py index 24e2cb1..f35ce89 100644 --- a/bumblebee/util.py +++ b/bumblebee/util.py @@ -18,6 +18,13 @@ def execute(cmd, wait=True): return out.decode("utf-8") return None +def bytefmt(num): + for unit in [ "", "Ki", "Mi", "Gi" ]: + if num < 1024.0: + return "{:.2f}{}B".format(num, unit) + num /= 1024.0 + return "{:05.2f%}{}GiB".format(num) + def durationfmt(duration): minutes, seconds = divmod(duration, 60) hours, minutes = divmod(minutes, 60) diff --git a/tests/modules/test_disk.py b/tests/modules/test_disk.py new file mode 100644 index 0000000..3ee74b0 --- /dev/null +++ b/tests/modules/test_disk.py @@ -0,0 +1,36 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.disk import Module +from tests.util import MockEngine, MockConfig, assertPopen + +class TestDiskModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.config = MockConfig() + self.config.set("disk.path", "somepath") + self.module = Module(engine=self.engine, config={"config": self.config}) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_leftclick(self, mock_input, mock_output, mock_select): + mock_input.readline.return_value = json.dumps({ + "name": self.module.id, + "button": bumblebee.input.LEFT_MOUSE, + "instance": None + }) + mock_select.return_value = (1,2,3) + self.engine.input.start() + self.engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, "nautilus {}".format(self.module.parameter("path"))) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 8f6bb7b45d1cb343ed4e4d9da64cf8d6537cb70f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 18:22:05 +0100 Subject: [PATCH 061/104] [core/input] Remove "valid input required" logic from input Accidentially committed a experimental way to enforce waiting for a valid input, mainly for testing. see #23 --- bumblebee/input.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index 6876031..a501a85 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -28,12 +28,10 @@ def read_input(inp): try: event = json.loads(line) inp.callback(event) - inp.has_valid_event = True inp.redraw() except ValueError: pass inp.has_event = True - inp.has_valid_event = True inp.clean_exit = True class I3BarInput(object): @@ -44,15 +42,12 @@ class I3BarInput(object): self.clean_exit = False self.global_id = str(uuid.uuid4()) self.need_event = False - self.need_valid_event = False self.has_event = False - self.has_valid_event = False self._condition = threading.Condition() def start(self): """Start asynchronous input processing""" self.has_event = False - self.has_valid_event = False self.running = True self._condition.acquire() self._thread = threading.Thread(target=read_input, args=(self,)) @@ -70,20 +65,16 @@ class I3BarInput(object): def wait(self, timeout): self._condition.wait(timeout) - def _wait(self, valid=False): + def _wait(self): while not self.has_event: time.sleep(0.1) - if valid: - while not self.has_valid_event: - time.sleep(0.1) self.has_event = False - self.has_valid_event = False def stop(self): """Stop asynchronous input processing""" self._condition.release() if self.need_event: - self._wait(self.need_valid_event) + self._wait() self.running = False self._thread.join() return self.clean_exit From e15147fe107e53f113c3d4ab84d7aab0717c3376 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 18:49:25 +0100 Subject: [PATCH 062/104] [tests/disk] Add critical/warning threshold tests for disk module see #23 --- bumblebee/modules/disk.py | 25 +++++++++++-------------- tests/modules/test_disk.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py index 586909f..a0b6aad 100644 --- a/bumblebee/modules/disk.py +++ b/bumblebee/modules/disk.py @@ -26,25 +26,22 @@ class Module(bumblebee.engine.Module): cmd="nautilus {}".format(self._path)) def diskspace(self): - st = os.statvfs(self._path) - size = st.f_frsize*st.f_blocks - used = size - st.f_frsize*st.f_bavail - self._perc = 100.0*used/size - return "{} {}/{} ({:05.02f}%)".format(self._path, - bumblebee.util.bytefmt(used), - bumblebee.util.bytefmt(size), self._perc + bumblebee.util.bytefmt(self._used), + bumblebee.util.bytefmt(self._size), self._perc ) def update(self, widgets): - pass + st = os.statvfs(self._path) + self._size = st.f_frsize*st.f_blocks + self._used = self._size - st.f_frsize*st.f_bavail + self._perc = 100.0*self._used/self._size def state(self, widget): - pass - def warning(self, widget): - return self._perc > self._config.parameter("warning", 80) - - def critical(self, widget): - return self._perc > self._config.parameter("critical", 90) + if self._perc > float(self.parameter("critical", 90)): + return "critical" + if self._perc > float(self.parameter("warning", 80)): + return "warning" + return None # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_disk.py b/tests/modules/test_disk.py index 3ee74b0..4d3909c 100644 --- a/tests/modules/test_disk.py +++ b/tests/modules/test_disk.py @@ -9,6 +9,12 @@ from bumblebee.input import I3BarInput from bumblebee.modules.disk import Module from tests.util import MockEngine, MockConfig, assertPopen +class MockVFS(object): + def __init__(self, perc): + self.f_blocks = 1024*1024 + self.f_frsize = 1 + self.f_bavail = self.f_blocks - self.f_blocks*(perc/100.0) + class TestDiskModule(unittest.TestCase): def setUp(self): self.engine = MockEngine() @@ -17,6 +23,8 @@ class TestDiskModule(unittest.TestCase): self.config = MockConfig() self.config.set("disk.path", "somepath") self.module = Module(engine=self.engine, config={"config": self.config}) + for widget in self.module.widgets(): + widget.link_module(self.module) @mock.patch("select.select") @mock.patch("subprocess.Popen") @@ -33,4 +41,21 @@ class TestDiskModule(unittest.TestCase): mock_input.readline.assert_any_call() assertPopen(mock_output, "nautilus {}".format(self.module.parameter("path"))) + @mock.patch("os.statvfs") + def test_warning(self, mock_stat): + self.config.set("disk.critical", "80") + self.config.set("disk.warning", "70") + mock_stat.return_value = MockVFS(75.0) + self.module.update(self.module.widgets()) + self.assertEquals(self.module.widgets()[0].state(), ["warning"]) + + @mock.patch("os.statvfs") + def test_warning(self, mock_stat): + self.config.set("disk.critical", "80") + self.config.set("disk.warning", "70") + mock_stat.return_value = MockVFS(85.0) + self.module.update(self.module.widgets()) + self.assertEquals(self.module.widgets()[0].state(), ["critical"]) + + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 16a4613e572199ef491207447871ba03203b6a0b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:08:29 +0100 Subject: [PATCH 063/104] [tests] Minor refactoring Draw some commonly-used assertion logic into common functions. see #23 --- tests/modules/test_brightness.py | 46 +++++++++++--------------------- tests/modules/test_cpu.py | 24 +++++------------ tests/modules/test_disk.py | 11 +++----- tests/util.py | 19 +++++++++++++ 4 files changed, 44 insertions(+), 56 deletions(-) diff --git a/tests/modules/test_brightness.py b/tests/modules/test_brightness.py index 9b0c105..0594fc3 100644 --- a/tests/modules/test_brightness.py +++ b/tests/modules/test_brightness.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.brightness import Module -from tests.util import MockEngine, MockConfig, assertPopen +from tests.util import MockEngine, MockConfig, assertPopen, assertMouseEvent class TestBrightnessModule(unittest.TestCase): def setUp(self): @@ -28,31 +28,19 @@ class TestBrightnessModule(unittest.TestCase): @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheel_up(self, mock_input, mock_output, mock_select): - mock_input.readline.return_value = json.dumps({ - "name": self.module.id, - "button": bumblebee.input.WHEEL_UP, - "instance": None - }) - mock_select.return_value = (1,2,3) - self.engine.input.start() - self.engine.input.stop() - mock_input.readline.assert_any_call() - assertPopen(mock_output, "xbacklight +2%") + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.WHEEL_UP, + "xbacklight +2%" + ) @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheel_down(self, mock_input, mock_output, mock_select): - mock_input.readline.return_value = json.dumps({ - "name": self.module.id, - "button": bumblebee.input.WHEEL_DOWN, - "instance": None - }) - mock_select.return_value = (1,2,3) - self.engine.input.start() - self.engine.input.stop() - mock_input.readline.assert_any_call() - assertPopen(mock_output, "xbacklight -2%") + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.WHEEL_DOWN, + "xbacklight -2%" + ) @mock.patch("select.select") @mock.patch("subprocess.Popen") @@ -60,13 +48,9 @@ class TestBrightnessModule(unittest.TestCase): def test_custom_step(self, mock_input, mock_output, mock_select): self.config.set("brightness.step", "10") module = Module(engine=self.engine, config={ "config": self.config }) - mock_input.readline.return_value = json.dumps({ - "name": module.id, - "button": bumblebee.input.WHEEL_DOWN, - "instance": None - }) - mock_select.return_value = (1,2,3) - self.engine.input.start() - self.engine.input.stop() - mock_input.readline.assert_any_call() - assertPopen(mock_output, "xbacklight -10%") + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + module, bumblebee.input.WHEEL_DOWN, + "xbacklight -10%" + ) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index 52b7967..a00d2c2 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.cpu import Module -from tests.util import MockEngine, MockConfig, assertPopen +from tests.util import MockEngine, MockConfig, assertPopen, assertMouseEvent, assertStateContains class TestCPUModule(unittest.TestCase): def setUp(self): @@ -16,8 +16,6 @@ class TestCPUModule(unittest.TestCase): self.engine.input.need_event = True self.config = MockConfig() self.module = Module(engine=self.engine, config={ "config": self.config }) - for widget in self.module.widgets(): - widget.link_module(self.module) @mock.patch("sys.stdout") def test_format(self, mock_output): @@ -28,31 +26,23 @@ class TestCPUModule(unittest.TestCase): @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): - mock_input.readline.return_value = json.dumps({ - "name": self.module.id, - "button": bumblebee.input.LEFT_MOUSE, - "instance": None - }) - mock_select.return_value = (1,2,3) - self.engine.input.start() - self.engine.input.stop() - mock_input.readline.assert_any_call() - assertPopen(mock_output, "gnome-system-monitor") + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.LEFT_MOUSE, + "gnome-system-monitor" + ) @mock.patch("psutil.cpu_percent") def test_warning(self, mock_psutil): self.config.set("cpu.critical", "20") self.config.set("cpu.warning", "18") mock_psutil.return_value = 19.0 - self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), ["warning"]) + assertStateContains(self, self.module, "warning") @mock.patch("psutil.cpu_percent") def test_critical(self, mock_psutil): self.config.set("cpu.critical", "20") self.config.set("cpu.warning", "19") mock_psutil.return_value = 21.0 - self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), ["critical"]) + assertStateContains(self, self.module, "critical") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_disk.py b/tests/modules/test_disk.py index 4d3909c..77cf286 100644 --- a/tests/modules/test_disk.py +++ b/tests/modules/test_disk.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.disk import Module -from tests.util import MockEngine, MockConfig, assertPopen +from tests.util import MockEngine, MockConfig, assertPopen, assertStateContains class MockVFS(object): def __init__(self, perc): @@ -23,8 +23,6 @@ class TestDiskModule(unittest.TestCase): self.config = MockConfig() self.config.set("disk.path", "somepath") self.module = Module(engine=self.engine, config={"config": self.config}) - for widget in self.module.widgets(): - widget.link_module(self.module) @mock.patch("select.select") @mock.patch("subprocess.Popen") @@ -46,16 +44,13 @@ class TestDiskModule(unittest.TestCase): self.config.set("disk.critical", "80") self.config.set("disk.warning", "70") mock_stat.return_value = MockVFS(75.0) - self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), ["warning"]) + assertStateContains(self, self.module, "warning") @mock.patch("os.statvfs") def test_warning(self, mock_stat): self.config.set("disk.critical", "80") self.config.set("disk.warning", "70") mock_stat.return_value = MockVFS(85.0) - self.module.update(self.module.widgets()) - self.assertEquals(self.module.widgets()[0].state(), ["critical"]) - + assertStateContains(self, self.module, "critical") # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index fded9f2..4a0e16f 100644 --- a/tests/util.py +++ b/tests/util.py @@ -1,5 +1,6 @@ # pylint: disable=C0103,C0111,W0613 +import json import shlex import subprocess @@ -16,6 +17,24 @@ def assertPopen(output, cmd): stderr=subprocess.STDOUT ) +def assertStateContains(test, module, state): + for widget in module.widgets(): + widget.link_module(module) + module.update(module.widgets()) + test.assertTrue(state in module.widgets()[0].state()) + +def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd): + mock_input.readline.return_value = json.dumps({ + "name": module.id, + "button": button, + "instance": None + }) + mock_select.return_value = (1,2,3) + engine.input.start() + engine.input.stop() + mock_input.readline.assert_any_call() + assertPopen(mock_output, cmd) + class MockInput(object): def start(self): pass From d41c142d4a68c293d4aeed119edc59cade2db9d2 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:20:19 +0100 Subject: [PATCH 064/104] [modules/memory] Re-enable memory usage module Add module that shows RAM consumption and opens the gnome-system-monitor on click. see #23 --- bumblebee/modules/memory.py | 44 +++++++++++++++++++++++++++++++++ tests/modules/test_memory.py | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 bumblebee/modules/memory.py create mode 100644 tests/modules/test_memory.py diff --git a/bumblebee/modules/memory.py b/bumblebee/modules/memory.py new file mode 100644 index 0000000..90515df --- /dev/null +++ b/bumblebee/modules/memory.py @@ -0,0 +1,44 @@ +# pylint: disable=C0111,R0903 + +"""Displays available RAM, total amount of RAM and percentage available. + +Parameters: + * cpu.warning : Warning threshold in % of memory used (defaults to 80%) + * cpu.critical: Critical threshold in % of memory used (defaults to 90%) +""" + +import psutil + +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.memory_usage) + ) + self._mem = psutil.virtual_memory() + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd="gnome-system-monitor") + + def memory_usage(self): + used = self._mem.total - self._mem.available + return "{}/{} ({:05.02f}%)".format( + bumblebee.util.bytefmt(used), + bumblebee.util.bytefmt(self._mem.total), + self._mem.percent + ) + + def update(self, widgets): + self._mem = psutil.virtual_memory() + + 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 diff --git a/tests/modules/test_memory.py b/tests/modules/test_memory.py new file mode 100644 index 0000000..4533357 --- /dev/null +++ b/tests/modules/test_memory.py @@ -0,0 +1,47 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.memory import Module +from tests.util import MockEngine, MockConfig, assertPopen, assertMouseEvent, assertStateContains + +class VirtualMemory(object): + def __init__(self, percent): + self.percent = percent + +class TestMemoryModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_leftclick(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.LEFT_MOUSE, + "gnome-system-monitor" + ) + + @mock.patch("psutil.virtual_memory") + def test_warning(self, mock_vmem): + self.config.set("memory.critical", "80") + self.config.set("memory.warning", "70") + mock_vmem.return_value = VirtualMemory(75) + assertStateContains(self, self.module, "warning") + + @mock.patch("psutil.virtual_memory") + def test_critical(self, mock_vmem): + self.config.set("memory.critical", "80") + self.config.set("memory.warning", "70") + mock_vmem.return_value = VirtualMemory(85) + assertStateContains(self, self.module, "critical") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 2c766d0c97c6ecb717041aa485b6fb02adf13d28 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:27:50 +0100 Subject: [PATCH 065/104] [CI] Initial travis configuration --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..725e4f9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + - "2.7" + - "3.2" + - "3.3" + - "3.4" + - "3.5" +script: nosetests -v tests/ From 14b8feeef1b22bc1169821f471870fa115131b37 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:30:25 +0100 Subject: [PATCH 066/104] [modules/datetime] Add help text --- bumblebee/modules/datetime.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py index 7d776ad..2ee074b 100644 --- a/bumblebee/modules/datetime.py +++ b/bumblebee/modules/datetime.py @@ -1,6 +1,12 @@ # pylint: disable=C0111,R0903 -"""Displays the current time, using the optional format string as input for strftime.""" +"""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 +""" from __future__ import absolute_import import datetime From c23af541e40dba25dfc3d442bcaa71428a792e0f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:32:22 +0100 Subject: [PATCH 067/104] [CI] Add psutil and netifaces to Travis dependencies --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 725e4f9..5ed60ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,7 @@ python: - "3.3" - "3.4" - "3.5" +install: + - pip install psutil + - pip install netifaces script: nosetests -v tests/ From 2e2351a69e073947a062c7e1e7ef7778653c67be Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:35:38 +0100 Subject: [PATCH 068/104] [tests] Mock Popen() in module tests --- tests/modules/test_modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 0e01873..7db9492 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -28,7 +28,8 @@ class TestGenericModules(unittest.TestCase): widget.set("variable", "value") self.assertEquals(widget.get("variable", None), "value") - def test_update(self): + @mock.patch("subprocess.Popen") + def test_update(self, mock_output): for mod in self.objects: widgets = self.objects[mod].widgets() self.objects[mod].update(widgets) From 6e3a9ec4d3da19156f106e7ed6ebace3e260f603 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:36:40 +0100 Subject: [PATCH 069/104] [CI] Add "mock" to Travis dependencies --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 5ed60ad..ca7ec2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,5 @@ python: install: - pip install psutil - pip install netifaces + - pip install mock script: nosetests -v tests/ From 921afe475f7f9211af893f615bac2e477c2ab395 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:37:03 +0100 Subject: [PATCH 070/104] [CI] Restrict Travis to Python2.7 only until build works --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca7ec2a..8d2114d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python python: - "2.7" - - "3.2" - - "3.3" - - "3.4" - - "3.5" + # - "3.2" + # - "3.3" + # - "3.4" + # - "3.5" install: - pip install psutil - pip install netifaces From 716bafa90e38d51f0c3bfafd5b137e5823426fb3 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:45:13 +0100 Subject: [PATCH 071/104] [tests] Fix unit tests (at least on my system) --- .travis.yml | 1 - tests/modules/test_modules.py | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8d2114d..3cdc51e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,4 @@ python: install: - pip install psutil - pip install netifaces - - pip install mock script: nosetests -v tests/ diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 7db9492..74d543a 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -2,11 +2,19 @@ import unittest import importlib +import mock from bumblebee.engine import all_modules from bumblebee.config import Config from tests.util import assertWidgetAttributes, MockEngine +class MockCommunicate(object): + def __init__(self): + self.returncode = 0 + + def communicate(self): + return (str.encode("1"), "error") + class TestGenericModules(unittest.TestCase): def setUp(self): engine = MockEngine() @@ -30,6 +38,7 @@ class TestGenericModules(unittest.TestCase): @mock.patch("subprocess.Popen") def test_update(self, mock_output): + mock_output.return_value = MockCommunicate() for mod in self.objects: widgets = self.objects[mod].widgets() self.objects[mod].update(widgets) From a1455c9687f8ff6a8b398481151cd70a7e8f9dfe Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:54:31 +0100 Subject: [PATCH 072/104] [modules/battery] Handle inexistent battery more gracefully --- bumblebee/modules/battery.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index 9fa4c6e..581a3eb 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -19,7 +19,7 @@ class Module(bumblebee.engine.Module): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.capacity) ) - battery = self.parameter("device", "BAT0") + battery = self.parameter("device", "BAT1") self._path = "/sys/class/power_supply/{}".format(battery) self._capacity = 100 @@ -32,8 +32,11 @@ class Module(bumblebee.engine.Module): if not os.path.exists(self._path): self._ac = True - with open(self._path + "/capacity") as f: - self._capacity = int(f.read()) + try: + with open(self._path + "/capacity") as f: + self._capacity = int(f.read()) + except IOError: + self._capacity = 100 self._capacity = self._capacity if self._capacity < 100 else 100 def state(self, widget): From c44b529c1f666d876760a8df3ca6675d8f185985 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 19:55:40 +0100 Subject: [PATCH 073/104] [CI] Add more Python versions to Travis Now that the tests run through with Python2.7, extend the list of Python versions to be supported. --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3cdc51e..5ed60ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ language: python python: - "2.7" - # - "3.2" - # - "3.3" - # - "3.4" - # - "3.5" + - "3.2" + - "3.3" + - "3.4" + - "3.5" install: - pip install psutil - pip install netifaces From e8f9a50cf7e6ee3e162e30fadcc426b724646f9f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 20:00:09 +0100 Subject: [PATCH 074/104] [CI] Removing Python 3.2, as it complains about unicode strings Honestly: I have *no idea* how to fix this, so for the time being, Python 3.2 won't be supported. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5ed60ad..6e7c9c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" From 547b3dc296a37a58492d6cc60fa6ca42c89b4a82 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 20:05:02 +0100 Subject: [PATCH 075/104] [doc] Re-add README.md --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..382f7e0 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# bumblebee-status + +[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=engine-rework)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) + +bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). + +Focus is on: +* Ease of use (no configuration files!) +* Theme support +* Extensibility (of course...) + +I hope you like it and appreciate any kind of feedback: Bug reports, Feature requests, etc. :) + +Thanks a lot! + +# Documentation +See [the wiki](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki) for documentation. + +Other resources: + +* A list of [available modules](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/Available-Modules) +* [How to write a theme](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/How-to-write-a-theme) +* [How to write a module](https://github.com/tobi-wan-kenobi/bumblebee-status/wiki/How-to-write-a-module) + +# Installation +``` +$ git clone git://github.com/tobi-wan-kenobi/bumblebee-status +``` + +# Usage + +Next, open your i3wm configuration and modify the *status_command* for your i3bar like this: + +``` +bar { + status_command = -m -p -t +} +``` + +You can retrieve a list of modules and themes by entering: +``` +$ cd bumblebee-status +$ ./bumblebee-status -l themes +$ ./bumblebee-status -l modules +``` + +As a simple example, this is what my i3 configuration looks like: + +``` +bar { + font pango:Inconsolata 10 + position top + tray_output none + status_command ~/.i3/bumblebee-status/bumblebee-status -m nic disk:/ cpu memory battery date time pasink pasource dnf -p time.format="%H:%M CW %V" date.format="%a, %b %d %Y" -t solarized-powerline +} + +``` + + +Restart i3wm and - that's it! + + +# Examples +Here are some screenshots for all themes that currently exist: + +Gruvbox Powerline (`-t gruvbox-powerline`) (contributed by [@paxy97](https://github.com/paxy97)): + +![Gruvbox Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-gruvbox.png) + +Solarized Powerline (`-t solarized-powerline`): + +![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png) + +Solarized (`-t solarized`): + +![Solarized](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/solarized.png) + +Powerline (`-t powerline`): + +![Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline.png) + +Default (nothing or `-t default`): + +![Default](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/default.png) From 14ca6d5cf0d223ac60986226211f132ee4bc4c32 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 20:09:42 +0100 Subject: [PATCH 076/104] [doc] Add code climate shield to README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 382f7e0..5b5c69f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bumblebee-status -[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=engine-rework)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) +[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=engine-rework)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). From 83488fe97fd7ae4581cda358bac8ac7e995560c7 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 22:12:02 +0100 Subject: [PATCH 077/104] [CI] Add Code Climate token for test coverage Let's see if/how code coverage works. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6e7c9c6..8963a56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,3 +8,7 @@ install: - pip install psutil - pip install netifaces script: nosetests -v tests/ +addons: + code_climate: + repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf + From fbd7801d8d34f083fe1e6fb9e204099130bd29ae Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 22:15:25 +0100 Subject: [PATCH 078/104] [CI] Disable test coverage reporting Seems to work only for the default branch, so disable until merge back to master. --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8963a56..2029c77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ install: - pip install psutil - pip install netifaces script: nosetests -v tests/ -addons: - code_climate: - repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf - +#addons: +# code_climate: +# repo_token: 40cb00907f7a10e04868e856570bb997ab9c42fd3b63d980f2b2269433195fdf From 8f759e61341439287c8f5f4be15da1314e385adb Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 10 Dec 2016 22:20:55 +0100 Subject: [PATCH 079/104] [CI] Add config file for Code Climate --- .codeclimate.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..928f344 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,19 @@ +engines: + duplication: + enabled: true + exclude_fingerprints: + - 729e672cc5eafa97b4ff7b4487b43e73 + - b6bcd0bad16e901a7e93b31776477fde + - 39aa354fb2009c102f8b297a4eade693 + config: + languages: + - python + fixme: + enabled: true + radon: + enabled: true +ratings: + paths: + - "**.py" +exclude_paths: +- tests/ From 2cc2cf82823be565f45e136fdd00c4a59de9e7ab Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 07:18:06 +0100 Subject: [PATCH 080/104] [core/engine] Add aliasing mechanism to modules Allow modules to define aliases. This replaces the symlink mechanism that was in place previously, because it was a bit ugly (and confused code climate). see #23 --- .codeclimate.yml | 4 ---- bumblebee/engine.py | 20 +++++++++++++++----- bumblebee/modules/date.py | 1 - bumblebee/modules/datetime.py | 5 +++-- bumblebee/modules/time.py | 1 - 5 files changed, 18 insertions(+), 13 deletions(-) delete mode 120000 bumblebee/modules/date.py delete mode 120000 bumblebee/modules/time.py diff --git a/.codeclimate.yml b/.codeclimate.yml index 928f344..ec80874 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,10 +1,6 @@ engines: duplication: enabled: true - exclude_fingerprints: - - 729e672cc5eafa97b4ff7b4487b43e73 - - b6bcd0bad16e901a7e93b31776477fde - - 39aa354fb2009c102f8b297a4eade693 config: languages: - python diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 5c8d848..4645cf8 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -26,11 +26,9 @@ class Module(object): this base class. """ def __init__(self, engine, config={}, widgets=[]): - self.name = self.__module__.split(".")[-1] + self.name = config.get("name", self.__module__.split(".")[-1]) self._config = config - if "name" not in self._config: - self._config["name"] = self.name - self.id = self._config["name"] + self.id = self.name self._widgets = [] if widgets: self._widgets = widgets if isinstance(widgets, list) else [widgets] @@ -50,7 +48,7 @@ class Module(object): def parameter(self, name, default=None): """Return the config parameter 'name' for this module""" - name = "{}.{}".format(self._config["name"], name) + name = "{}.{}".format(self.name, name) return self._config["config"].get(name, default) class Engine(object): @@ -65,6 +63,7 @@ class Engine(object): self._running = True self._modules = [] self.input = inp + self._aliases = self._read_aliases() self.load_modules(config.modules()) self.input.register_callback(None, bumblebee.input.WHEEL_UP, @@ -80,8 +79,19 @@ class Engine(object): self._modules.append(self._load_module(module["module"], module["name"])) return self._modules + def _read_aliases(self): + result = {} + for module in all_modules(): + mod = importlib.import_module("bumblebee.modules.{}".format(module["name"])) + for alias in getattr(mod, "ALIASES", []): + result[alias] = module["name"] + return result + 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 try: diff --git a/bumblebee/modules/date.py b/bumblebee/modules/date.py deleted file mode 120000 index bde6404..0000000 --- a/bumblebee/modules/date.py +++ /dev/null @@ -1 +0,0 @@ -datetime.py \ No newline at end of file diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py index 2ee074b..6d6b594 100644 --- a/bumblebee/modules/datetime.py +++ b/bumblebee/modules/datetime.py @@ -12,6 +12,8 @@ from __future__ import absolute_import import datetime import bumblebee.engine +ALIASES = [ "date", "time" ] + def default_format(module): default = "%x %X" if module == "date": @@ -25,8 +27,7 @@ class Module(bumblebee.engine.Module): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.get_time) ) - module = self.__module__.split(".")[-1] - self._fmt = self.parameter("format", default_format(module)) + self._fmt = self.parameter("format", default_format(self.name)) def get_time(self): return datetime.datetime.now().strftime(self._fmt) diff --git a/bumblebee/modules/time.py b/bumblebee/modules/time.py deleted file mode 120000 index bde6404..0000000 --- a/bumblebee/modules/time.py +++ /dev/null @@ -1 +0,0 @@ -datetime.py \ No newline at end of file From d91294f010cc342d1e807064bed56fcfa1d77a3a Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 07:28:15 +0100 Subject: [PATCH 081/104] [modules/battery] Fix ac and unknown display If the computer runs on AC, display that instead of showing "100%" in the status. Also, if reading the charging status fails for some reason (except the computer being on AC), go into critical state and display "n/a". see #23 --- bumblebee/modules/battery.py | 14 ++++++++++++-- bumblebee/theme.py | 5 +++-- themes/icons/awesome-fonts.json | 5 ++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index 581a3eb..423d509 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -19,11 +19,15 @@ class Module(bumblebee.engine.Module): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.capacity) ) - battery = self.parameter("device", "BAT1") + battery = self.parameter("device", "BAT0") self._path = "/sys/class/power_supply/{}".format(battery) self._capacity = 100 def capacity(self): + if self._ac: + return "ac" + if self._capacity == -1: + return "n/a" return "{:03d}%".format(self._capacity) def update(self, widgets): @@ -31,16 +35,22 @@ class Module(bumblebee.engine.Module): self._ac = False if not os.path.exists(self._path): self._ac = True + self._capacity = 100 + return try: with open(self._path + "/capacity") as f: self._capacity = int(f.read()) except IOError: - self._capacity = 100 + self._capacity = -1 self._capacity = self._capacity if self._capacity < 100 else 100 def state(self, widget): state = [] + + if self._capacity < 0: + return ["critical", "unknown"] + if self._capacity < int(self.parameter("critical", 10)): state.append("critical") elif self._capacity < int(self.parameter("warning", 20)): diff --git a/bumblebee/theme.py b/bumblebee/theme.py index a7a7ccd..3486f70 100644 --- a/bumblebee/theme.py +++ b/bumblebee/theme.py @@ -121,8 +121,9 @@ class Theme(object): state_themes = [] # avoid infinite recursion - if name not in widget.state(): - for state in widget.state(): + states = widget.state() + if name not in states: + for state in states: state_themes.append(self._get(widget, state, {})) value = self._defaults.get(name, default) diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index c019337..770cfa8 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -1,5 +1,8 @@ { - "defaults": { "separator": "", "padding": " " }, + "defaults": { + "separator": "", "padding": " ", + "unknown": { "prefix": "" } + }, "date": { "prefix": "" }, "time": { "prefix": "" }, "memory": { "prefix": "" }, From 1a4cddb0b661bd5bc2f278419b45e2f5a4a5dc5a Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 07:38:56 +0100 Subject: [PATCH 082/104] [core] Fix callback registration ("shadowed" events) Until now, as soon as a widget registered *any* callback, the default callbacks (e.g. scroll up/down to go to next/previous workspace) didn't work anymore, as there was a better match for the general registration (even though not for the button). To fix this, merge the callback registration into a flat registration, where a key is calculated from the ID of the registrar and the registered button. see #23 --- bumblebee/input.py | 31 ++++++++++++++++++++----------- bumblebee/modules/battery.py | 1 + 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index a501a85..ba41362 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -79,30 +79,39 @@ class I3BarInput(object): self._thread.join() return self.clean_exit - def _uid(self, obj): + def _uuidstr(self, name, button): + return "{}::{}".format(name, button) + + def _uid(self, obj, button): uid = self.global_id if obj: uid = obj.id - return uid + return self._uuidstr(uid, button) def deregister_callbacks(self, obj): - uid = self._uid(obj) - if uid in self._callbacks: - del self._callbacks[uid] + 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) + uid = self._uid(obj, button) if uid not in self._callbacks: self._callbacks[uid] = {} - self._callbacks[uid][button] = cmd + self._callbacks[uid] = cmd def callback(self, event): """Execute callback action for an incoming event""" - cmd = self._callbacks.get(self.global_id, {}) - cmd = self._callbacks.get(event["name"], cmd) - cmd = self._callbacks.get(event["instance"], cmd) - cmd = cmd.get(event["button"], None) + 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 if callable(cmd): diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index 423d509..cf1251e 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -22,6 +22,7 @@ class Module(bumblebee.engine.Module): battery = self.parameter("device", "BAT0") self._path = "/sys/class/power_supply/{}".format(battery) self._capacity = 100 + self._ac = False def capacity(self): if self._ac: From 4bd13c2f63a8566cb32874749564e7aa12748b6c Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:01:16 +0100 Subject: [PATCH 083/104] [tests] Refactor i3barinput Use the assertMouseEvent helper in i3barinput. see #23 --- tests/test_i3barinput.py | 82 ++++++++-------------------------------- tests/util.py | 11 +++--- 2 files changed, 22 insertions(+), 71 deletions(-) diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index 57aa161..e2758d6 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput -from tests.util import MockWidget, MockModule, assertPopen +from tests.util import MockWidget, MockModule, assertPopen, assertMouseEvent class TestI3BarInput(unittest.TestCase): def setUp(self): @@ -57,109 +57,59 @@ class TestI3BarInput(unittest.TestCase): @mock.patch("select.select") @mock.patch("sys.stdin") def test_global_callback(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": "somename", - "instance": "someinstance", - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(None, button=1, cmd=self.callback) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + assertMouseEvent(mock_input, None, mock_select, self, None, + bumblebee.input.LEFT_MOUSE, None, "someinstance") self.assertTrue(self._called > 0) @mock.patch("select.select") @mock.patch("sys.stdin") def test_remove_global_callback(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": "somename", - "instance": "someinstance", - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(None, button=1, cmd=self.callback) self.input.deregister_callbacks(None) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + assertMouseEvent(mock_input, None, mock_select, self, None, + bumblebee.input.LEFT_MOUSE, None, "someinstance") self.assertTrue(self._called == 0) @mock.patch("select.select") @mock.patch("sys.stdin") def test_global_callback_button_missmatch(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": "somename", - "instance": "someinstance", - "button": bumblebee.input.RIGHT_MOUSE, - }) - self.input.register_callback(None, button=1, cmd=self.callback) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + self.input.register_callback(self.anyModule, button=1, cmd=self.callback) + assertMouseEvent(mock_input, None, mock_select, self, None, + bumblebee.input.RIGHT_MOUSE, None, "someinstance") self.assertTrue(self._called == 0) @mock.patch("select.select") @mock.patch("sys.stdin") def test_module_callback(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": self.anyModule.id, - "instance": None, - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(self.anyModule, button=1, cmd=self.callback) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + assertMouseEvent(mock_input, None, mock_select, self, self.anyModule, + bumblebee.input.LEFT_MOUSE, None) self.assertTrue(self._called > 0) @mock.patch("select.select") @mock.patch("sys.stdin") def test_remove_module_callback(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": self.anyModule.id, - "instance": None, - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(self.anyModule, button=1, cmd=self.callback) self.input.deregister_callbacks(self.anyModule) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + assertMouseEvent(mock_input, None, mock_select, self, None, + bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) self.assertTrue(self._called == 0) @mock.patch("select.select") @mock.patch("sys.stdin") def test_widget_callback(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": "test", - "instance": self.anyWidget.id, - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(self.anyWidget, button=1, cmd=self.callback) - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() + assertMouseEvent(mock_input, None, mock_select, self, None, + bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) self.assertTrue(self._called > 0) @mock.patch("select.select") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_widget_cmd_callback(self, mock_input, mock_output, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = json.dumps({ - "name": "test", - "instance": self.anyWidget.id, - "button": bumblebee.input.LEFT_MOUSE, - }) self.input.register_callback(self.anyWidget, button=1, cmd="echo") - self.input.start() - self.assertEquals(self.input.stop(), True) - mock_input.readline.assert_any_call() - assertPopen(mock_output, "echo") + assertMouseEvent(mock_input, mock_output, mock_select, self, None, + bumblebee.input.LEFT_MOUSE, "echo", self.anyWidget.id) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/util.py b/tests/util.py index 4a0e16f..0b46d62 100644 --- a/tests/util.py +++ b/tests/util.py @@ -23,17 +23,18 @@ def assertStateContains(test, module, state): module.update(module.widgets()) test.assertTrue(state in module.widgets()[0].state()) -def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd): +def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd, instance_id=None): mock_input.readline.return_value = json.dumps({ - "name": module.id, + "name": module.id if module else "test", "button": button, - "instance": None + "instance": instance_id }) - mock_select.return_value = (1,2,3) + mock_select.return_value = (1, 2, 3) engine.input.start() engine.input.stop() mock_input.readline.assert_any_call() - assertPopen(mock_output, cmd) + if cmd: + assertPopen(mock_output, cmd) class MockInput(object): def start(self): From c8fc75a40133e9e894de7ba136c8f4b046b08488 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:01:43 +0100 Subject: [PATCH 084/104] [modules/load] Re-enable load module Display system load and show warning/critical error when load is above a certain threshold (compared to the number of available CPUs). see #23 --- bumblebee/modules/load.py | 43 ++++++++++++++++++++++++++++++++++ tests/modules/test_load.py | 47 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 bumblebee/modules/load.py create mode 100644 tests/modules/test_load.py diff --git a/bumblebee/modules/load.py b/bumblebee/modules/load.py new file mode 100644 index 0000000..8c51e9f --- /dev/null +++ b/bumblebee/modules/load.py @@ -0,0 +1,43 @@ +# 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) + ) + try: + self._cpus = multiprocessing.cpu_count() + except multiprocessing.NotImplementedError as e: + self._cpus = 1 + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd="gnome-system-monitor") + + def load(self): + 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): + if self._load[0] > float(self.parameter("critical", self._cpus*0.8)): + return "critical" + if self._load[0] > float(self.parameter("warning", self._cpus*0.7)): + return "warning" + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_load.py b/tests/modules/test_load.py new file mode 100644 index 0000000..d8120e2 --- /dev/null +++ b/tests/modules/test_load.py @@ -0,0 +1,47 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.load import Module +from tests.util import MockEngine, MockConfig, assertStateContains, assertMouseEvent + +class TestLoadModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_leftclick(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.LEFT_MOUSE, + "gnome-system-monitor" + ) + + @mock.patch("multiprocessing.cpu_count") + @mock.patch("os.getloadavg") + def test_warning(self, mock_loadavg, mock_cpucount): + self.config.set("load.critical", "1") + self.config.set("load.warning", "0.8") + mock_cpucount.return_value = 1 + mock_loadavg.return_value = [ 0.9, 0, 0 ] + assertStateContains(self, self.module, "warning") + + @mock.patch("multiprocessing.cpu_count") + @mock.patch("os.getloadavg") + def test_critical(self, mock_loadavg, mock_cpucount): + self.config.set("load.critical", "1") + self.config.set("load.warning", "0.8") + mock_cpucount.return_value = 1 + mock_loadavg.return_value = [ 1.1, 0, 0 ] + assertStateContains(self, self.module, "critical") + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 7db80b6d3b78e7eaa24641957fa4b0e4694a5bdf Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:11:26 +0100 Subject: [PATCH 085/104] [tests/battery] Mock exists() call for Travis CI --- tests/modules/test_battery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/modules/test_battery.py b/tests/modules/test_battery.py index da2e3cc..c2e0b59 100644 --- a/tests/modules/test_battery.py +++ b/tests/modules/test_battery.py @@ -41,11 +41,13 @@ class TestBatteryModule(unittest.TestCase): for widget in self.module.widgets(): self.assertEquals(len(widget.full_text()), len("100%")) + @mock.patch("os.path.exists") @mock.patch("{}.open".format("__builtin__" if sys.version_info[0] < 3 else "builtins")) @mock.patch("subprocess.Popen") - def test_critical(self, mock_output, mock_open): + def test_critical(self, mock_output, mock_open, mock_exists): mock_open.return_value = MockOpen() mock_open.return_value.returns("19") + mock_exists.return_value = True self.config.set("battery.critical", "20") self.config.set("battery.warning", "25") self.module.update(self.module.widgets()) From ef224fdbb655792090a288493cbaa53afdcd7b84 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:13:54 +0100 Subject: [PATCH 086/104] [themes] Add missing load module icon --- themes/icons/awesome-fonts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 770cfa8..b3784eb 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -10,6 +10,7 @@ "disk": { "prefix": "" }, "dnf": { "prefix": "" }, "brightness": { "prefix": "" }, + "load": { "prefix": "" }, "cmus": { "playing": { "prefix": "" }, "paused": { "prefix": "" }, From e603a2cb2635ef5edef1bf4904449599ddaa730a Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:14:55 +0100 Subject: [PATCH 087/104] [tests/battery] Forgot a mock in previous commit --- tests/modules/test_battery.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/modules/test_battery.py b/tests/modules/test_battery.py index c2e0b59..499a0fa 100644 --- a/tests/modules/test_battery.py +++ b/tests/modules/test_battery.py @@ -53,10 +53,12 @@ class TestBatteryModule(unittest.TestCase): self.module.update(self.module.widgets()) self.assertTrue("critical" in self.module.widgets()[0].state()) + @mock.patch("os.path.exists") @mock.patch("{}.open".format("__builtin__" if sys.version_info[0] < 3 else "builtins")) @mock.patch("subprocess.Popen") - def test_warning(self, mock_output, mock_open): + def test_warning(self, mock_output, mock_open, mock_exists): mock_open.return_value = MockOpen() + mock_exists.return_value = True mock_open.return_value.returns("22") self.config.set("battery.critical", "20") self.config.set("battery.warning", "25") From edfccd2d3160eb48dc3bec960da1c33ff087bae3 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:17:37 +0100 Subject: [PATCH 088/104] [modules/spacer] Re-enable "spacer", the text-display widget see #23 --- bumblebee/modules/spacer.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 bumblebee/modules/spacer.py diff --git a/bumblebee/modules/spacer.py b/bumblebee/modules/spacer.py new file mode 100644 index 0000000..729ef54 --- /dev/null +++ b/bumblebee/modules/spacer.py @@ -0,0 +1,26 @@ +# 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): + return self._text + + def update(self, widgets): + pass + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 23d7d53fca29da72fc3fcee619078fe805ba96b1 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:25:54 +0100 Subject: [PATCH 089/104] [modules] critical/warning threshold refactoring Quite a lot of modules use the "if higher X -> critical, if higher Y -> warning" idiom now, so extracted that into a common function for reuse. see #23 --- bumblebee/engine.py | 7 +++++++ bumblebee/modules/cmus.py | 12 ++++-------- bumblebee/modules/cpu.py | 6 +----- bumblebee/modules/disk.py | 6 +----- bumblebee/modules/load.py | 5 +---- 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 4645cf8..7b06160 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -51,6 +51,13 @@ class Module(object): name = "{}.{}".format(self.name, name) return self._config["config"].get(name, default) + 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 diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index 0505294..857d895 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -72,14 +72,10 @@ class Module(bumblebee.engine.Module): if line.startswith("tag"): key, value = line.split(" ", 2)[1:] self._tags.update({ key: value }) - if line.startswith("duration"): - self._tags.update({ - "duration": bumblebee.util.durationfmt(int(line.split(" ")[1])) - }) - if line.startswith("position"): - self._tags.update({ - "position": bumblebee.util.durationfmt(int(line.split(" ")[1])) - }) + for key in ["duration", "position"]: + if line.startswith(key): + dur = int(line.split(" ")[1]) + self._tags.update({key:bumblebee.util.durationfmt(dur)}) if line.startswith("set repeat "): self._repeat = False if "false" in line else True if line.startswith("set shuffle "): diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index 214b205..c5bdc06 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -28,10 +28,6 @@ class Module(bumblebee.engine.Module): self._utilization = psutil.cpu_percent(percpu=False) def state(self, widget): - if self._utilization > int(self.parameter("critical", 80)): - return "critical" - if self._utilization > int(self.parameter("warning", 70)): - return "warning" - return None + return self.threshold_state(self._utilization, 70, 80) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py index a0b6aad..de7f7ed 100644 --- a/bumblebee/modules/disk.py +++ b/bumblebee/modules/disk.py @@ -38,10 +38,6 @@ class Module(bumblebee.engine.Module): self._perc = 100.0*self._used/self._size def state(self, widget): - if self._perc > float(self.parameter("critical", 90)): - return "critical" - if self._perc > float(self.parameter("warning", 80)): - return "warning" - return None + return self.threshold_state(self._perc, 80, 90) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/load.py b/bumblebee/modules/load.py index 8c51e9f..7ab4f34 100644 --- a/bumblebee/modules/load.py +++ b/bumblebee/modules/load.py @@ -35,9 +35,6 @@ class Module(bumblebee.engine.Module): self._load = os.getloadavg() def state(self, widget): - if self._load[0] > float(self.parameter("critical", self._cpus*0.8)): - return "critical" - if self._load[0] > float(self.parameter("warning", self._cpus*0.7)): - return "warning" + return self.threshold_state(self._load[0], self._cpus*0.7, self._cpus*0.8) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From c2c70da4eff42fe29e0cf9f26631a809186546b7 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:45:43 +0100 Subject: [PATCH 090/104] [tests/disk] Fix copy/paste error - duplicate method name --- tests/modules/test_disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/test_disk.py b/tests/modules/test_disk.py index 77cf286..f8fee4f 100644 --- a/tests/modules/test_disk.py +++ b/tests/modules/test_disk.py @@ -47,7 +47,7 @@ class TestDiskModule(unittest.TestCase): assertStateContains(self, self.module, "warning") @mock.patch("os.statvfs") - def test_warning(self, mock_stat): + def test_critical(self, mock_stat): self.config.set("disk.critical", "80") self.config.set("disk.warning", "70") mock_stat.return_value = MockVFS(85.0) From 666daff9a685bc0268a76f25059ee0c743270d79 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 08:51:56 +0100 Subject: [PATCH 091/104] [modules/pulseaudio] Re-enable pulseaudio module Display volume for default PulseAudio source/sink, change volume and mute/unmute device. see #23 --- bumblebee/modules/pulseaudio.py | 88 ++++++++++++++++++++++++++++++++ tests/modules/test_pulseaudio.py | 56 ++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 bumblebee/modules/pulseaudio.py create mode 100644 tests/modules/test_pulseaudio.py diff --git a/bumblebee/modules/pulseaudio.py b/bumblebee/modules/pulseaudio.py new file mode 100644 index 0000000..82629b6 --- /dev/null +++ b/bumblebee/modules/pulseaudio.py @@ -0,0 +1,88 @@ +# pylint: disable=C0111,R0903 + +"""Displays volume and mute status of PulseAudio devices. + +Aliases: pasink, pasource +""" + +import re + +import bumblebee.util +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +ALIASES = [ "pasink", "pasource" ] + +class Module(bumblebee.engine.Module): + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, + bumblebee.output.Widget(full_text=self.volume) + ) + + self._left = 0 + self._right = 0 + self._mono = 0 + self._mute = False + channel = "sink" if self.name == "pasink" else "source" + + engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, cmd="pavucontrol") + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd="pactl set-{}-mute @DEFAULT_{}@ toggle".format(channel, channel.upper())) + engine.input.register_callback(self, button=bumblebee.input.WHEEL_UP, + cmd="pactl set-{}-volume @DEFAULT_{}@ +2%".format(channel, channel.upper())) + engine.input.register_callback(self, button=bumblebee.input.WHEEL_DOWN, + cmd="pactl set-{}-volume @DEFAULT_{}@ -2%".format(channel, channel.upper())) + + def _default_device(self): + output = bumblebee.util.execute("pactl info") + pattern = "Default Sink: " if self.name == "pasink" else "Default Source: " + for line in output.split("\n"): + if line.startswith(pattern): + return line.replace(pattern, "") + return "n/a" + + def volume(self): + if int(self._mono) > 0: + return "{}%".format(self._mono) + elif self._left == self._right: + return "{}%".format(self._left) + else: + return "{}%/{}%".format(self._left, self._right) + return "n/a" + + def update(self, widgets): + 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:" in line and found == True: + break + if device in line: + found = True + + if "Mute:" in line and found == True: + self._mute = False if " no" in line.lower() else True + + if "Volume:" in line and found == True: + m = None + if "mono" in line: + m = re.search(r'mono:.*\s*\/\s*(\d+)%', line) + else: + m = re.search(r'left:.*\s*\/\s*(\d+)%.*right:.*\s*\/\s*(\d+)%', line) + if not m: continue + + if "mono" in line: + self._mono = m.group(1) + else: + self._left = m.group(1) + self._right = m.group(2) + + def state(self, widget): + if self._mute: + return [ "warning", "muted" ] + return [ "unmuted" ] + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/modules/test_pulseaudio.py b/tests/modules/test_pulseaudio.py new file mode 100644 index 0000000..6515589 --- /dev/null +++ b/tests/modules/test_pulseaudio.py @@ -0,0 +1,56 @@ +# pylint: disable=C0103,C0111 + +import json +import unittest +import mock + +import bumblebee.input +from bumblebee.input import I3BarInput +from bumblebee.modules.pulseaudio import Module +from tests.util import MockEngine, MockConfig, assertPopen, assertMouseEvent, assertStateContains + +class TestPulseAudioModule(unittest.TestCase): + def setUp(self): + self.engine = MockEngine() + self.engine.input = I3BarInput() + self.engine.input.need_event = True + self.config = MockConfig() + self.module = Module(engine=self.engine, config={ "config": self.config }) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_leftclick(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.LEFT_MOUSE, + "pactl set-source-mute @DEFAULT_SOURCE@ toggle" + ) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_rightclick(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.RIGHT_MOUSE, + "pavucontrol" + ) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_wheelup(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.WHEEL_UP, + "pactl set-source-volume @DEFAULT_SOURCE@ +2%" + ) + + @mock.patch("select.select") + @mock.patch("subprocess.Popen") + @mock.patch("sys.stdin") + def test_wheeldown(self, mock_input, mock_output, mock_select): + assertMouseEvent(mock_input, mock_output, mock_select, self.engine, + self.module, bumblebee.input.WHEEL_DOWN, + "pactl set-source-volume @DEFAULT_SOURCE@ -2%" + ) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 4f6ec89d3c92fb7fda28015e0b776493709afe10 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 09:06:23 +0100 Subject: [PATCH 092/104] [doc] Add Code Climate issue count --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5b5c69f..0157695 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # bumblebee-status -[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=engine-rework)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) +[![Build Status](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status.svg?branch=engine-rework)](https://travis-ci.org/tobi-wan-kenobi/bumblebee-status) [![Code Climate](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/gpa.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) bumblebee-status is a modular, theme-able status line generator for the [i3 window manager](https://i3wm.org/). From 71582cbcd7f56a53f27898ecf9affc40e57e3ba8 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 11:37:24 +0100 Subject: [PATCH 093/104] [modules/ping] Re-enable ping module Show RTT measured by ICMP echo request/replies for a given host. For that to work correctly, change the "full_text" callback for a widget so that the widget itself is also passed as argument in the callback method. That actually makes a lot of sense, since the widget can now be used as a repository of state information. see #23 --- bumblebee/modules/battery.py | 2 +- bumblebee/modules/brightness.py | 2 +- bumblebee/modules/caffeine.py | 5 +- bumblebee/modules/cmus.py | 3 +- bumblebee/modules/cpu.py | 2 +- bumblebee/modules/datetime.py | 2 +- bumblebee/modules/disk.py | 4 +- bumblebee/modules/load.py | 3 +- bumblebee/modules/memory.py | 2 +- bumblebee/modules/ping.py | 81 +++++++++++++++++++++++++++++++++ bumblebee/modules/pulseaudio.py | 2 +- bumblebee/modules/spacer.py | 5 +- bumblebee/modules/test.py | 3 -- bumblebee/output.py | 2 +- tests/modules/test_modules.py | 1 + 15 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 bumblebee/modules/ping.py diff --git a/bumblebee/modules/battery.py b/bumblebee/modules/battery.py index cf1251e..90e58f6 100644 --- a/bumblebee/modules/battery.py +++ b/bumblebee/modules/battery.py @@ -24,7 +24,7 @@ class Module(bumblebee.engine.Module): self._capacity = 100 self._ac = False - def capacity(self): + def capacity(self, widget): if self._ac: return "ac" if self._capacity == -1: diff --git a/bumblebee/modules/brightness.py b/bumblebee/modules/brightness.py index 56b5314..99712f0 100644 --- a/bumblebee/modules/brightness.py +++ b/bumblebee/modules/brightness.py @@ -24,7 +24,7 @@ class Module(bumblebee.engine.Module): engine.input.register_callback(self, button=bumblebee.input.WHEEL_DOWN, cmd="xbacklight -{}%".format(step)) - def brightness(self): + def brightness(self, widget): return "{:03.0f}%".format(self._brightness) def update(self, widgets): diff --git a/bumblebee/modules/caffeine.py b/bumblebee/modules/caffeine.py index cc2754e..bf57be6 100644 --- a/bumblebee/modules/caffeine.py +++ b/bumblebee/modules/caffeine.py @@ -16,7 +16,7 @@ class Module(bumblebee.engine.Module): cmd=self._toggle ) - def caffeine(self): + def caffeine(self, widget): return "" def state(self, widget): @@ -33,9 +33,6 @@ class Module(bumblebee.engine.Module): return False return False - def update(self, widgets): - pass - def _toggle(self, widget): if self._active(): bumblebee.util.execute("xset s default") diff --git a/bumblebee/modules/cmus.py b/bumblebee/modules/cmus.py index 857d895..991a1a1 100644 --- a/bumblebee/modules/cmus.py +++ b/bumblebee/modules/cmus.py @@ -41,8 +41,9 @@ class Module(bumblebee.engine.Module): self._status = None self._shuffle = False self._repeat = False + self._tags = defaultdict(lambda: '') - def description(self): + def description(self, widget): return string.Formatter().vformat(self._fmt, (), self._tags) def update(self, widgets): diff --git a/bumblebee/modules/cpu.py b/bumblebee/modules/cpu.py index c5bdc06..79e9229 100644 --- a/bumblebee/modules/cpu.py +++ b/bumblebee/modules/cpu.py @@ -21,7 +21,7 @@ class Module(bumblebee.engine.Module): engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd="gnome-system-monitor") - def utilization(self): + def utilization(self, widget): return "{:06.02f}%".format(self._utilization) def update(self, widgets): diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py index 6d6b594..4141a53 100644 --- a/bumblebee/modules/datetime.py +++ b/bumblebee/modules/datetime.py @@ -29,7 +29,7 @@ class Module(bumblebee.engine.Module): ) self._fmt = self.parameter("format", default_format(self.name)) - def get_time(self): + def get_time(self, widget): return datetime.datetime.now().strftime(self._fmt) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/disk.py b/bumblebee/modules/disk.py index de7f7ed..e052b88 100644 --- a/bumblebee/modules/disk.py +++ b/bumblebee/modules/disk.py @@ -21,11 +21,13 @@ class Module(bumblebee.engine.Module): ) self._path = self.parameter("path", "/") self._perc = 0 + self._used = 0 + self._size = 0 engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd="nautilus {}".format(self._path)) - def diskspace(self): + def diskspace(self, widget): return "{} {}/{} ({:05.02f}%)".format(self._path, bumblebee.util.bytefmt(self._used), bumblebee.util.bytefmt(self._size), self._perc diff --git a/bumblebee/modules/load.py b/bumblebee/modules/load.py index 7ab4f34..c44ae5a 100644 --- a/bumblebee/modules/load.py +++ b/bumblebee/modules/load.py @@ -19,6 +19,7 @@ class Module(bumblebee.engine.Module): 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 multiprocessing.NotImplementedError as e: @@ -26,7 +27,7 @@ class Module(bumblebee.engine.Module): engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd="gnome-system-monitor") - def load(self): + def load(self, widget): return "{:.02f}/{:.02f}/{:.02f}".format( self._load[0], self._load[1], self._load[2] ) diff --git a/bumblebee/modules/memory.py b/bumblebee/modules/memory.py index 90515df..0ab1176 100644 --- a/bumblebee/modules/memory.py +++ b/bumblebee/modules/memory.py @@ -23,7 +23,7 @@ class Module(bumblebee.engine.Module): engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd="gnome-system-monitor") - def memory_usage(self): + def memory_usage(self, widget): used = self._mem.total - self._mem.available return "{}/{} ({:05.02f}%)".format( bumblebee.util.bytefmt(used), diff --git a/bumblebee/modules/ping.py b/bumblebee/modules/ping.py new file mode 100644 index 0000000..a851b0e --- /dev/null +++ b/bumblebee/modules/ping.py @@ -0,0 +1,81 @@ +# pylint: disable=C0111,R0903 + +"""Periodically checks the RTT of a configurable host using ICMP echos + +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): + + main = None + for thread in threading.enumerate(): + if thread.name == "MainThread": + main = thread + + interval = widget.get("interval") + next_check = 0 + while main.is_alive(): + try: + if int(time.time()) < next_check: + time.sleep(1) + continue + 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") + )) + next_check = int(time.time()) + interval + + for line in res.split("\n"): + 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", "") + + self._thread = threading.Thread(target=get_rtt, args=(self,widget,)) + self._thread.start() + + 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") + ) + + def state(self, widget): + if widget.get("rtt-unreachable"): return ["critical"] + return self.threshold_state(widget.get("rtt-avg"), 1000.0, 2000.0) + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/pulseaudio.py b/bumblebee/modules/pulseaudio.py index 82629b6..b13eda3 100644 --- a/bumblebee/modules/pulseaudio.py +++ b/bumblebee/modules/pulseaudio.py @@ -42,7 +42,7 @@ class Module(bumblebee.engine.Module): return line.replace(pattern, "") return "n/a" - def volume(self): + def volume(self, widget): if int(self._mono) > 0: return "{}%".format(self._mono) elif self._left == self._right: diff --git a/bumblebee/modules/spacer.py b/bumblebee/modules/spacer.py index 729ef54..b89c233 100644 --- a/bumblebee/modules/spacer.py +++ b/bumblebee/modules/spacer.py @@ -17,10 +17,7 @@ class Module(bumblebee.engine.Module): ) self._text = self.parameter("text", "") - def text(self): + def text(self, widget): return self._text - def update(self, widgets): - pass - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index ad07faa..9f7485f 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -10,7 +10,4 @@ class Module(bumblebee.engine.Module): bumblebee.output.Widget(full_text="test") ) - def update(self, widgets): - pass - # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/output.py b/bumblebee/output.py index 7e76cc2..974e651 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -41,7 +41,7 @@ class Widget(bumblebee.store.Store): self._full_text = value else: if callable(self._full_text): - return self._full_text() + return self._full_text(self) else: return self._full_text diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 74d543a..b67baa6 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -35,6 +35,7 @@ class TestGenericModules(unittest.TestCase): assertWidgetAttributes(self, widget) widget.set("variable", "value") self.assertEquals(widget.get("variable", None), "value") + self.assertTrue(isinstance(widget.full_text(), str)) @mock.patch("subprocess.Popen") def test_update(self, mock_output): From 17ee621a5a1ded0725bad3e8fe0868059a9f556e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 11:50:15 +0100 Subject: [PATCH 094/104] [modules/ping] Spawn thread on-the-fly Instead of having a thread that runs in the background continuously, spawn a new one for every update interval. That speeds up the tests quite a lot. see #23 --- bumblebee/modules/ping.py | 51 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/bumblebee/modules/ping.py b/bumblebee/modules/ping.py index a851b0e..6217438 100644 --- a/bumblebee/modules/ping.py +++ b/bumblebee/modules/ping.py @@ -20,35 +20,22 @@ 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") + )) - main = None - for thread in threading.enumerate(): - if thread.name == "MainThread": - main = thread + for line in res.split("\n"): + if not line.startswith("rtt"): continue + m = re.search(r'([0-9\.]+)/([0-9\.]+)/([0-9\.]+)/([0-9\.]+)\s+(\S+)', line) - interval = widget.get("interval") - next_check = 0 - while main.is_alive(): - try: - if int(time.time()) < next_check: - time.sleep(1) - continue - 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") - )) - next_check = int(time.time()) + interval - - for line in res.split("\n"): - 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) + 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): @@ -62,8 +49,7 @@ class Module(bumblebee.engine.Module): widget.set("rtt-avg", 0.0) widget.set("rtt-unit", "") - self._thread = threading.Thread(target=get_rtt, args=(self,widget,)) - self._thread.start() + self._next_check = 0 def rtt(self, widget): if widget.get("rtt-unreachable"): @@ -78,4 +64,11 @@ class Module(bumblebee.engine.Module): 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()) + widgets[0].get("interval") + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From 1d6ca352b974b8a8524e63a43fe472f8357d3419 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:23:33 +0100 Subject: [PATCH 095/104] [modules/xrandr] Re-enable xrandr module Displays the connected screens and allows the user to enable/disable them. see #23 --- bumblebee/engine.py | 6 ++++ bumblebee/modules/xrandr.py | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 bumblebee/modules/xrandr.py diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 7b06160..0698171 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -42,6 +42,12 @@ class Module(object): if widget.name == name: return widget + 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 diff --git a/bumblebee/modules/xrandr.py b/bumblebee/modules/xrandr.py new file mode 100644 index 0000000..def9235 --- /dev/null +++ b/bumblebee/modules/xrandr.py @@ -0,0 +1,72 @@ +# pylint: disable=C0111,R0903 + +"""Shows a widget for each connected screen and allows the user to enable/disable screens. + +""" + +import os +import re +import sys + +import bumblebee.util +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +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): + new_widgets = [] + 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.maxint) + + while len(widgets) > 0: + del widgets[0] + for widget in new_widgets: + widgets.append(widget) + + def update(self, widgets): + self.update_widgets(widgets) + + def state(self, widget): + return widget.get("state", "off") + + def _toggle(self, event): + path = os.path.dirname(os.path.abspath(__file__)) + toggle_cmd = "{}/../../bin/toggle-display.sh".format(path) + + 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 == 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 From 9fe091573098901b61477c549b6501bc7b883004 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:26:28 +0100 Subject: [PATCH 096/104] [tests] Add mocking for module tests --- tests/modules/test_modules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index b67baa6..99723a0 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -26,7 +26,9 @@ class TestGenericModules(unittest.TestCase): for widget in self.objects[mod["name"]].widgets(): self.assertEquals(widget.get("variable", None), None) - def test_widgets(self): + @mock.patch("subprocess.Popen") + def test_widgets(self, mock_output): + mock_output.return_value = MockCommunicate() for mod in self.objects: widgets = self.objects[mod].widgets() for widget in widgets: From 9878bbf971cb8db5f889ef9c7efbec01cd5ab1bf Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:31:37 +0100 Subject: [PATCH 097/104] [tests] Fix automated testrun in Travis Forgot to mock Popen() in setUp --- tests/modules/test_modules.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/modules/test_modules.py b/tests/modules/test_modules.py index 99723a0..4c54a93 100644 --- a/tests/modules/test_modules.py +++ b/tests/modules/test_modules.py @@ -16,7 +16,9 @@ class MockCommunicate(object): return (str.encode("1"), "error") class TestGenericModules(unittest.TestCase): - def setUp(self): + @mock.patch("subprocess.Popen") + def setUp(self, mock_output): + mock_output.return_value = MockCommunicate() engine = MockEngine() config = Config() self.objects = {} @@ -42,6 +44,10 @@ class TestGenericModules(unittest.TestCase): @mock.patch("subprocess.Popen") def test_update(self, mock_output): mock_output.return_value = MockCommunicate() + rv = mock.Mock() + rv.configure_mock(**{ + "communicate.return_value": ("out", None) + }) for mod in self.objects: widgets = self.objects[mod].widgets() self.objects[mod].update(widgets) From dd6b13265de166a994b0c8e19b4ea5cca4454c9e Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:42:49 +0100 Subject: [PATCH 098/104] [modules/pacman] Re-enable pacman update information see #23 --- bin/pacman-updates | 22 ++++++++++++++ bin/toggle-display.sh | 12 ++++++++ bumblebee/modules/pacman.py | 60 +++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100755 bin/pacman-updates create mode 100755 bin/toggle-display.sh create mode 100644 bumblebee/modules/pacman.py diff --git a/bin/pacman-updates b/bin/pacman-updates new file mode 100755 index 0000000..baf0b93 --- /dev/null +++ b/bin/pacman-updates @@ -0,0 +1,22 @@ +#!/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 diff --git a/bin/toggle-display.sh b/bin/toggle-display.sh new file mode 100755 index 0000000..bd13a29 --- /dev/null +++ b/bin/toggle-display.sh @@ -0,0 +1,12 @@ +#!/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 + $i3bar_update +fi diff --git a/bumblebee/modules/pacman.py b/bumblebee/modules/pacman.py new file mode 100644 index 0000000..da3e66f --- /dev/null +++ b/bumblebee/modules/pacman.py @@ -0,0 +1,60 @@ +# pylint: disable=C0111,R0903 + +"""Displays update information per repository for pacman." +""" + +import os +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.updates) + ) + self._count = 0 + self._out = "" + + def updates(self, widget): + return self._out + + def update(self, widgets): + path = os.path.dirname(os.path.abspath(__file__)) + if self._count == 0: + self._out = "?/?/?/?" + try: + result = bumblebee.util.execute("{}/../../bin/pacman-updates".format(path)) + self._community = 0 + self._core = 0 + self._extra = 0 + self._other = 0 + + for line in result.splitlines(): + if line.startswith("http"): + if "community" in line: + self._community += 1 + continue + if "core" in line: + self._core += 1; + continue + if "extra" in line: + self._extra += 1 + continue + self._other += 1 + self._out = str(self._core)+"/"+str(self._extra)+"/"+str(self._community)+"/"+str(self._other) + except RuntimeError: + self._out = "?/?/?/?" + + # TODO: improve this waiting mechanism a bit + self._count += 1 + self._count = 0 if self._count > 300 else self._count + + def sumUpdates(self): + return self._core + self._community + self._extra + self._other + + def state(self, widget): + if self.sumUpdates() > 0: + return "critical" + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 From d10fc814eda11b4fcb8b1c5c717bace82e8f7af4 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:53:31 +0100 Subject: [PATCH 099/104] [modules/dnf] Re-enable DNF update checking module see #23 --- bumblebee/modules/dnf.py | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 bumblebee/modules/dnf.py diff --git a/bumblebee/modules/dnf.py b/bumblebee/modules/dnf.py new file mode 100644 index 0000000..f6acf2a --- /dev/null +++ b/bumblebee/modules/dnf.py @@ -0,0 +1,79 @@ +# pylint: disable=C0111,R0903 + +"""Displays DNF package update information (///) + +Parameters: + * dnf.interval: Time in seconds between two consecutive update checks (defaulst to 1800) + +""" + +import time +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.decode().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._next_check = 0 + widget + + 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): + if int(time.time()) < self._next_check: + return + thread = threading.Thread(target=get_dnf_info, args=(widgets[0],)) + thread.start() + self._next_check = int(time.time()) + self.parameter("interval", 30*60) + + 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 From 65ae8c242b82716e3627377da355d8b62f4e5b7b Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:58:03 +0100 Subject: [PATCH 100/104] [themes] Re-enable themes see #23 --- themes/default.json | 7 ++++++ themes/gruvbox-powerline.json | 43 +++++++++++++++++++++++++++++++++++ themes/icons/paxy97.json | 5 ++++ themes/powerline.json | 40 ++++++++++++++++++++++++++++++++ themes/solarized.json | 41 +++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 themes/default.json create mode 100644 themes/gruvbox-powerline.json create mode 100644 themes/icons/paxy97.json create mode 100644 themes/powerline.json create mode 100644 themes/solarized.json diff --git a/themes/default.json b/themes/default.json new file mode 100644 index 0000000..ddda5fe --- /dev/null +++ b/themes/default.json @@ -0,0 +1,7 @@ +{ + "icons": [ "ascii" ], + "defaults": { + "urgent": true, + "fg": "#aabbcc" + } +} diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json new file mode 100644 index 0000000..e8921c3 --- /dev/null +++ b/themes/gruvbox-powerline.json @@ -0,0 +1,43 @@ +{ + "icons": [ "paxy97", "awesome-fonts" ], + "defaults": { + "prefix": " ", + "suffix" : " ", + "warning": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "critical": { + "fg": "#fbf1c7", + "bg": "#cc241d" + }, + "default-separators": false, + "separator-block-width": 0 + }, + "cycle": [ + { + "fg": "#ebdbb2", + "bg": "#1d2021" + }, + { + "fg": "#fbf1c7", + "bg": "#282828" + } + ], + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "AC": { + "fg": "#1d2021", + "bg": "#b8bb26" + } + } +} diff --git a/themes/icons/paxy97.json b/themes/icons/paxy97.json new file mode 100644 index 0000000..9a889ab --- /dev/null +++ b/themes/icons/paxy97.json @@ -0,0 +1,5 @@ +{ + "memory": { + "prefix": "  " + } +} diff --git a/themes/powerline.json b/themes/powerline.json new file mode 100644 index 0000000..afea669 --- /dev/null +++ b/themes/powerline.json @@ -0,0 +1,40 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "critical": { + "fg": "#ffffff", + "bg": "#ff0000" + }, + "warning": { + "fg": "#d75f00", + "bg": "#ffd700" + }, + "default_separators": false + }, + "cycle": [ + { + "fg": "#ffd700", + "bg": "#d75f00" + }, + { + "fg": "#ffffff", + "bg": "#0087af" + } + ], + "dnf": { + "good": { + "fg": "#494949", + "bg": "#41db00" + } + }, + "battery": { + "charged": { + "fg": "#494949", + "bg": "#41db00" + }, + "AC": { + "fg": "#494949", + "bg": "#41db00" + } + } +} diff --git a/themes/solarized.json b/themes/solarized.json new file mode 100644 index 0000000..2ea76c4 --- /dev/null +++ b/themes/solarized.json @@ -0,0 +1,41 @@ +{ + "icons": [ "ascii" ], + "defaults": { + "critical": { + "fg": "#002b36", + "bg": "#dc322f" + }, + "warning": { + "fg": "#002b36", + "bg": "#b58900" + }, + "default_separators": false, + "separator": "" + }, + "cycle": [ + { + "fg": "#93a1a1", + "bg": "#002b36" + }, + { + "fg": "#eee8d5", + "bg": "#586e75" + } + ], + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#002b36", + "bg": "#859900" + }, + "AC": { + "fg": "#002b36", + "bg": "#859900" + } + } +} From f441be7d11f1aab2380fb8d3aa5e749d49c610b4 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 12:58:36 +0100 Subject: [PATCH 101/104] [doc] Re-add screenshots --- screenshots/battery.png | Bin 0 -> 1733 bytes screenshots/brightness.png | Bin 0 -> 1323 bytes screenshots/caffeine.png | Bin 0 -> 1143 bytes screenshots/cmus.png | Bin 0 -> 5031 bytes screenshots/cpu.png | Bin 0 -> 1811 bytes screenshots/date.png | Bin 0 -> 2877 bytes screenshots/datetime.png | Bin 0 -> 4889 bytes screenshots/disk.png | Bin 0 -> 3721 bytes screenshots/dnf.png | Bin 0 -> 1113 bytes screenshots/load.png | Bin 0 -> 2005 bytes screenshots/memory.png | Bin 0 -> 3603 bytes screenshots/nic.png | Bin 0 -> 2474 bytes screenshots/pacman.png | Bin 0 -> 860 bytes screenshots/pasink.png | Bin 0 -> 1486 bytes screenshots/pasource.png | Bin 0 -> 1505 bytes screenshots/ping.png | Bin 0 -> 1655 bytes screenshots/pulseaudio.png | Bin 0 -> 2495 bytes screenshots/spacer.png | Bin 0 -> 521 bytes screenshots/themes/default.png | Bin 0 -> 7251 bytes screenshots/themes/powerline-gruvbox.png | Bin 0 -> 11594 bytes screenshots/themes/powerline-solarized.png | Bin 0 -> 11991 bytes screenshots/themes/powerline.png | Bin 0 -> 10701 bytes screenshots/themes/solarized.png | Bin 0 -> 9612 bytes screenshots/time.png | Bin 0 -> 2456 bytes screenshots/xrandr.png | Bin 0 -> 2693 bytes 25 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/battery.png create mode 100644 screenshots/brightness.png create mode 100644 screenshots/caffeine.png create mode 100644 screenshots/cmus.png create mode 100644 screenshots/cpu.png create mode 100644 screenshots/date.png create mode 100644 screenshots/datetime.png create mode 100644 screenshots/disk.png create mode 100644 screenshots/dnf.png create mode 100644 screenshots/load.png create mode 100644 screenshots/memory.png create mode 100644 screenshots/nic.png create mode 100644 screenshots/pacman.png create mode 100644 screenshots/pasink.png create mode 100644 screenshots/pasource.png create mode 100644 screenshots/ping.png create mode 100644 screenshots/pulseaudio.png create mode 100644 screenshots/spacer.png create mode 100644 screenshots/themes/default.png create mode 100644 screenshots/themes/powerline-gruvbox.png create mode 100644 screenshots/themes/powerline-solarized.png create mode 100644 screenshots/themes/powerline.png create mode 100644 screenshots/themes/solarized.png create mode 100644 screenshots/time.png create mode 100644 screenshots/xrandr.png diff --git a/screenshots/battery.png b/screenshots/battery.png new file mode 100644 index 0000000000000000000000000000000000000000..bdbafee42fd9ce6df799d0cd95888d09fe707a75 GIT binary patch literal 1733 zcmV;$20HnPP){x2YX&>x1cG^C)uYGHMs84m;QM9dEwaQWj zI+R5j*<66Ih9!X{k^~aWNNmr1O*S`K?S)Ad5L^eO}2%%1K;#?4r+iZ5Eca({V9Uej7c~z=65`xqK01hYo z^-mF~ue~$yJov7Qw8Z$mg2M3T2VB1#g#9L#}HTsI*~ps;xLru{B=rZg1-3O)97qvc0@` z0teB4DWkcI`|)uJ#O99GhQ~pa5bErbc);a~)lp1K;HR3qc%6N0zgnBQkpNv4(`IOBm{@1 ziF9}F#Q*?uGt{Mf*3fI*H<>o2%CUwbod+DJ-~|LnDR1b005WN+bY#P;`D0qei9EyVL4e?4182~Uf6Yc?*?*UiWCg|vm z$`z{*=gWPcl1Le~EwKTng%En!#q$ks6^&zA#_j=@$r7^eusEJkz%}X&gd2_VZx&7X zcFpQY-pv9b5b_PvGvRJ_4BEni4VhF@;&?`rB}A(yaw07N05;PyGe=smBMLQnr&!(4 z&aW&R!5IF1qs$Q*&HIx~avaE0c)+y^I{Kmj0I^)7?;{h_)ZZHuP;{M{J#n!=TZtg( z=B8v0%W~%3KIv4%igqHwG4G>(a5&Vv7&MCgK%U}KP2%63yz@7<=8DyPjtKz3qA%>q zoH_Sf8jo$BohOQ7o^e?gjBX5!Ez9&~E57P>(t;iFf;&!n@aQL zzto9bC}h(YQiVFN&XTJrHyYzly)hicvRSj>qrFW z>PE8_4*)3HGnJj@M+Q*B7Ec}>{_%QB;Ow-t;JRmos_G=09oZ~K26JdjH$;H?H@~KGaKQj_ zmCLf6I4liN23Bh0NhS4Ky|AK0y1M4#AZoVaKdZ~pw=+|HwkqRa zoGTDl@$P9qp^gJabLjb-TM>w+2=xbdEh!jr(wx%?+j=Ag^4S000ocp%GHl)o>Uw*zE@Zk4iyK=C9+TqA03*q)AGk{_oRB#LhP_ zz}}}II|~>CU{tHKyWm{|(WulHDoXY(Cjr9R8kh1iJ=-fgET-lbO?D@)KY@^!nMNQG zAP7>Pk)M-Smlfs%0B97-@l@Xa;e;U2A~T8wF5I&^yKG!ATVjMrOrXB^`Z<4a!)S99 z=42rVqHL6x7v>AIGfg(<=hwUN9LXfo=~78!<*BX#)ueVV9@UFA6@siZw=d9fcK}7v zQX#*oru@_PP7K3#MXj%}NQ)e}ZE4YBoz)xS`DQ4Jsx|XB`UZXe0FTWq7ZnubWW})O zBp-QKsc^z>2#QyAEQKu*u}HT=og*cOh!_vkWZzMBOPIn_mlR{FMqn@ z4{pS%I?85x`~d&}?DigIvuaDkF1JUkU)hyYEO52T4YSBjzs3A}zIx*q^Yf=2#jwrLR`deFQLV1Q>D&|=g_4!Z8=9I?$f~~T z>fMzyH`DWK<5FbpY-e>=Z$MErc9ij1oyX^Q!fw6Q7S&)efCu~~l`OlJIM`Aj}6L?Uj56951;;zBx&7CVYB5WGD&VqIIuGjO;b2RAow z_Wu#jaf(LTcKAeGZtAse;bw@4i~6L& z))h$zgy;`|N$vd41H*c&?X@O36mjsAXbCNKQnhxzy}N%McJCFcLRR(rs5+I)5$9!p z{LPOig$k_V@$Wdhb14SPq55d&0@jOiSgkRMQ#&Krj?S5X9~E5%AjU^P;yLx%C@=N7LAO z-Ou21PYJ_n-F%Ed#re6yZOVzAm)Rvi~# zX$Bb9qZ*6TU9-fx^JliAetX73%1a+avC|v<8ym|JbA>`8HP=aJ7WHPk^TRjWcx=|t zgP9)Hm?$@!#Ynn8IlbR5{q+@A=8?^^LMH%#D3;blTJTs*Duo=0ilC^@V1hkf1VNY0 zs~J3Q$K8R@R(N&Ig*R{iTQOQ0{vf$b{@Arvd7+6`QM}Nc} hU^zsn(Bb(x`VRs*2Jffwcy<5)002ovPDHLkV1oRXbDIDF literal 0 HcmV?d00001 diff --git a/screenshots/caffeine.png b/screenshots/caffeine.png new file mode 100644 index 0000000000000000000000000000000000000000..54447779afbf96b58344d131d7c390685fd770b4 GIT binary patch literal 1143 zcmV--1c>{IP)6c$KL}; zVOS9n4u}XiRHV{rp@wQZjWsr|-I8V5VzMpEk|n!Xvb!bQUGI0hCEG66rA?D&YqQkV z8SU&O6q&1E6Qod)4>=q~D2RL&1*gPXn3J|W@cdEWD$^Lx+xf1c-s#yp12`r&c5$F^K?Bd7F~|bhY|ax191jKtT@J%lkn;Jw(Xz&2ojCeDG#c%K=2Rk; z{?}7J2nWDyvj@VFqt62Ya89j)8+cVV-jc)V3k1n#8PuNU*K^6Du|(49bfcj#^{X&z zY9Od|U;LTP{a2(j9ZD%IsG=pS%e#*FDP|ebYfo}spcruZeD95)XBKQv%0n$8;b@<( zMI@}N<&;N5yU`6~Yr;4miNz1X)72_#YnD_=4^BU7b$kBzHhA#+vSHcs(VMTmH8Qa5 z*vRFIR(HA~XC+@Fp zc&VSIlr(iIMO z-G_d4`<}y#y!YxQ2!e9C9I_J{H<+)DPrdWX#fqPC-=wfWr)ncj7=^z;WY=u7KNHDH z1bhI%UIHUJhkJV_jPp;UdqsH&$M4S033>H0v2fpM_xRSFp8l8AWb?zk`g3X(RSx`H zLT*n!pFbo_yR7+x*GF~g_LV2DI97xt7YiMm6(pL?WW zQkhH@Bmh7yn?2C2sbSEmk?zbGcfz|^CR6lFr|ll!!xbypUDLlUj`p1uHq;k?4d{)b z^Jpl%NhpIFrMyEXHIYvR0ElClVZ}mL$DooWjQKQNRrT@ z6zoEG*Oiy`_ve@F?#*}-lfaF`y*(T@`5G2FW>g`{Q<^HC|hD&|A7Ll;A ztpCX4`1aF?3qdDMkMr|ja=?=Imw2i(E}kJQ+p1P{sQQ|s$03vvpfI*002ov JPDHLkV1iwhDMA1M literal 0 HcmV?d00001 diff --git a/screenshots/cmus.png b/screenshots/cmus.png new file mode 100644 index 0000000000000000000000000000000000000000..52c0b5159e2d2eb8dece597c177b523109319651 GIT binary patch literal 5031 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 LNkvXXu0mjfW)Onx literal 0 HcmV?d00001 diff --git a/screenshots/disk.png b/screenshots/disk.png new file mode 100644 index 0000000000000000000000000000000000000000..7a659b8fcdfeb1468d05e49f29f1c98fe3098c53 GIT binary patch literal 3721 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 literal 0 HcmV?d00001 diff --git a/screenshots/dnf.png b/screenshots/dnf.png new file mode 100644 index 0000000000000000000000000000000000000000..49924168f31d1a081be9a94ab4063993d73d31ed GIT binary patch literal 1113 zcmV-f1g86mP)*HlGt&bp1w4y`s&)?Z*BoZ%eLrdRf6Mp1HxTG@04|M= z%mP7l`s~YB?n&6=U>RYzCI!DZ#pa_#ObuUniDhBICQgYM8z5?EaHZXt5j;Blc!VHu z(G}|MNU_5e0-Ahe^1eV3d1dhQdbcI@uY6NF10!`6nUvryRjZF~N7#8X#tHwjV0hxy z0RRfylG~H*x({$o!#3MTXJvP$&7p|GXvklFd-bTYNEgH*wX-L7 z#u;W|z8$x2B#<50-kIFFdTgm2E&~8OoHsmP)K^mk34J$VZPDgG{b;F0o4OUvk~uK3RV#Lvel0G_WlIC}Q29#{r`7HGUN{FJv;P2=&5BPd`6 zbrzT2{IT=;kWYb>nSimW);ZQyWAv*x0019f_P4d{t372hGMYrDo-})0A-x(8hL6ak zqlT#GKxTk19POlk}MX3SPBG4{iIXIDo@X5T*_VA=wGeq(lYWcGlG0013| z-@SkAcpGN7B>@0f7DiS7R29XNcxFlGIu+q9_1bk$B#zv=+d;6Ve7zc{eczDEMAJ~h)EiPby*PK6Z$dc3H2^=6NvYPy32xzMF%@UB%+SDVO~G_({DzKg07tl z>=thGK5Zmc?->YI_YF@dCQZ-0at~rZe{?${N0r6C6Y&Gv!3JfKb~jxipos^q&Xb!4 zhKWzB<&cUbq0X8o(SJ{IonW fP%Zzt1aaywgMS}|9}1IZ00000NkvXXu0mjfah(r0 literal 0 HcmV?d00001 diff --git a/screenshots/load.png b/screenshots/load.png new file mode 100644 index 0000000000000000000000000000000000000000..e136e7e9e1f0e88f5d84e7f4fc2a1b98686f19d8 GIT binary patch literal 2005 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 literal 0 HcmV?d00001 diff --git a/screenshots/nic.png b/screenshots/nic.png new file mode 100644 index 0000000000000000000000000000000000000000..d1512c0772b4b3bc2f09f274af9a215af3b366ec GIT binary patch literal 2474 zcmV;b303xqP)X0ssI2ND$sx00003b3#c}2nYz< z;ZNWI000nlMObuGZ)S9NVRB^vQ)qQ`bY*g5g3t*700~A(L_t(|+U;9wP*Z0b{?5t$ zN-pF=Aaad>fFK}(h+suPTM^wZXx(*f-R*Q{-Py6T({*=$bY}l_W@oxH+t$^+^kTPL zt1zn~R76w+xkT;+!X-dRVnRqDAtVqIAZLHXVv2GSLN}#5=b7{4e3$o}=X>&=?|aYt zoj@QV17N_giGjx?z_8hHe1EQI5ylk3fX$;AF!=0_cwB@)fcYqfO$r!>bCMF~796YV zF=;St20#F0ZHp6d*c)2wAqWx+cspXFVZ9RyN#w!o3_gc7F=eW1XdN8a_&ACag?Uk-A>%szmv_@Q1e5iJ(I zeey>l9v1*$G+Sz=op)Q>Yo%TC(Q&iYCg8A1L_$P}AT?gnJ3O}Fbo%~lX^qo0&25kS z2lu5X*LU>T9X{E{h(k}kl3!8RSk=&qAjnS+zSOT&&D!SCt#B~>tK%gj8tvukIu4V5 zcyDHNkKBbI0d`Ps5c4;uUZs*rECx+Jrq&s!H^?xMRw&BTsnnO!lYf8V(|eD*+{cJU z=W@Bc|C*tq6iM_-(QLJ`8T9q}=oTUWY-urxNN~Aa*B>-}UR|fuPR!WmTrStdw0TJN zq_I;*r%<*C`BX9~F($HWXvDF&zS)#QCaudyXRJ1vVqB@yFE|&+b(3L29yf?Npj4w< zWhTXof;pF~>YR&`*qV}ckyW6Zb2~T zQbiRyLln3WWUYwEKZb#{LQ$SdA~_t+bv>>EHY@Af4*KGRl-C};3gyM}Te>OJp5%nW ztTY&gJ(C6DA%gG_!GT?=U)9#X|F3cYfPTv4X|C2AQsX7=c;(#5WAA->b<$*}QOHKq zjMrOdZBO{?wVS#r6OSGA%hF;BnN(5NxT+6{NJy4MUAx=hE(*EqH;M}{T&q-QbSwt_ z)%={)_?U)H832IKWgE;h0D!FR+a%#(<#i2*_hxQzQuKwP@9>XdAgxf8r;?3)*-}~e%yJK~7OEYN{a##dBJ|ctS51j7J=+zZoZm9;Aq8=XRS zIGkYuUP<2W53XE?V3CYKRty`p`gw8+ai68Jf2i;E&2_t2h<^124C zaXMNoa{3ZCLy+GL*Bz?*#W0XoXv(Xe{OR&F_j!+E>`|vIH#G^}M;DyV`Yu_1dJ36H zTr(#X7qN~UzEV4(o-mv{d2IXEC@Pr*0AMlb*(nKUN{Sl0`^;7V@IQLaHB0 z!}wfLFvm1&^(CP8^bZ^i0RUF4UQiK3-vAoh#Tm(U`KWO^Hh0p4wT_S$)?_NKi z8Ygi#O&Cq32XbX2h zqRC<%)#&!7CTR^*zz6RLW<48Z%$Csk1;?~y)?Iu#XBUG;{m1p&tA@v*QQaeNl?_}h zza{M(+_NLm-PAoi3IM2l*y1iGZ;kFBRe2Y2et}7&t`t2fp)>RY{xJ-s6^imO40}4I zFqmWUSY%LOw%QtdWhl?~y7xj=6O&)uzMmQ|Ie)s0&177zx@+;4m{jYH$y=k8Iz0eD zKBnH85Uce_xS;Jl#7WZPB`gL#Enf0U&aV4y-R_kogvZTGPyYAU_aF$6hy)Um;GWk3 z0LS)azfoKmAreqXr0@_yWN65UTI+6V>6Ke-wz7g;K8JlMJ3}nwJ?ifB%0V`wOo@#N zW(5HN(&8m?5n^8$`UkHJ3o_IG_|_>mneW94Mfv?ZQ_hwZ$3=+gR7zUBWKT+BV^^P# z12>p0&3%I}ly2FD`1wtVv~N%>r3Z4^OuErD^JUHb z&OwFmV}D{ab`0UuAet(A#K@jr4+@yKuRB6}n=-t*1uvxMB8UEmq5E)J* zlUMD9`v2V%67XF_4FC7&SE|Nz`i;h;fdjPZ6@@UT`tC7hu{~lqpVE_OC07*qoM6N<$f`yF3^Z)<= literal 0 HcmV?d00001 diff --git a/screenshots/pacman.png b/screenshots/pacman.png new file mode 100644 index 0000000000000000000000000000000000000000..8f4169e3f1b7eeb12ff9a5947b4386ff071d929a GIT binary patch literal 860 zcmV-i1Ec(jP)q%SB`UCnqYCUSL&8;=pY7SZ=CAy(9LFNyT zLj(CG#|;eR^z4B*5{?pF4{np!v+sRBuif_E_w&8)_gIgfzW{}J|J4l6F%T$Ihzk0n zgM&jf6e{PE14)t=#|odzIp?|L^wALjR@Z7MhL!s$Czr)F_jV}qe_v237IF|aldV(w z$hqm;_~|d+l!%0n?-`owE~A0qyQyz6JW;T0d`Q0&jeVS+WvoR<)!ay}7q>s)^rFRw z0z=T#2g6&z@XJ?kw}RnkVJB3Sn`pYqiw&>r%8@ z9h5b8N5?t>npSyUjZ`e^>}WC0FEQ52qgryYUrA*LIlU-JlBMQSC6$D+H~^quqik!e zpD@kT%A|QUw|lit4|?+J#xyH`Tahc=}E?y8g literal 0 HcmV?d00001 diff --git a/screenshots/pasink.png b/screenshots/pasink.png new file mode 100644 index 0000000000000000000000000000000000000000..2fd6359522b9984ea4e8df7aed4d5465f718163f GIT binary patch literal 1486 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 Hu0mjfCfMGn literal 0 HcmV?d00001 diff --git a/screenshots/ping.png b/screenshots/ping.png new file mode 100644 index 0000000000000000000000000000000000000000..1c8bf2bec8065882dc2306424b8bafe161b1f309 GIT binary patch literal 1655 zcmV--28j8IP)2#(uY1-zsGwt*t=_Gk*U($5aHrjN_ z9!DFk8Y@OoR74OrL{R|+1VmZx?E{WW>acX0nMnEOK3vYZ-+%7;|8u_o=Ug1%<%6}r zK`}&3e(e=bXveK{vYhz)T9_@ zWWXu@iWM2(oDiEF9pycf!-{yjtboI!>x|}m4WjOWG5=t>K~@ThQR>szx|HYxL}TJbd-zPx z+^RR3X;g|E8Zij@EXi%|k~>k%y+I0@RGOcD@$2e3p~Q75 z#->pe{m)}88BwmtcnmYr*V;RFQ#imtlSpQ>J33{f6Iy*Dhv^3_Hy9e%T)y>0GN>|` zEcMT&5``*1b7^}N<)n^GXzSahdXwc&ZJW)3=J66e8Vn{P3yXHMbz%9kI~*@p#+kIR z!t^v>Y1-L2QV=1T!%~b*QGmI`} z+3-YsG^0g4;NBpage=$j9VA&b_aTN&sIDPRlZ&z>=_dF4tgBym130( za^9;f>5;1}R-gZ85)r~Xy9*mSq*FR003bDv9ZDs2$(GcTo4}@rklXu4+#88z(DHck zIo#Na(!9L%g!x6AT5A9RgiuJ^w{U-}?`RUqPrf1O8=AQFsO8{xzG`|#r#G>}Lq9yW z`}=#%RW-s6>2P*xY!CrII;rzY##^8dt z#eB|@1Q|RLJqRHH0K%wb2ml-o000c8`S0#FHFnCqmGK3e z?a!{>o&0Pff_OjS#2Ch@{emk$)~Gf5tQ5}aL&avRO){w3K&;wUNo}jd*GMA-`2EUK z2!_6`uE(&Y5^Q8bbEmfLTxBT?;U;wku}n$uBG~fL$uDm_blr>P%H~d437^}mPyql? zY^At!I-Sd>AOHZla;l-d|NN0%I{jRaVyv}$aB9Zr<;z0=0JGKFDDJg8oZip5G@mEz z8B}ZZ0D$g+v8E1L@s?CSV0myB1f8uY0RUXDsYDIE9KP|;0t-Ws;4?|^z8AtaN>I~-U+P2ni#hPUc;|O>d2SFQ)CA!UmQl6#tDBDi02qdbP)Hx0*n>JT@qqI8`i>Vj@p@zZ(Iy1|9Ntw(3n6P~=YDwH zBI+OU_!@WXMdgC5b8nWA$wb54d|R*L@v|N;r-1wCCr(~`Y%r0Sz-9#wll=qBpE%c9 z*L@?(Vf9qNNB)QPMNDTj23+%h33dR1U6)qWsV%MQns!Fp8pcw` zw$w%xQ54xl1B4~)LI4SbkmY6}$;~zoEMiD*Zdc6%Qht?(oAZ6=-2XklvwY_!7!aQU z-rHs|{K29El%Y4CG;Z#`K;IP`%GPqhz8}$>YIdRtvRZ9YxqLJC0v;E}V!d;+*K_fV z({8sn_ep$6#LdbQi^b*%;|(VByCvHt7Y~cA1Az8{A-PJuSy^HL$c~F)(WveMB171} zDoWp$9!K!PZ5-L|xOkYIUBhG3a)rm%i3Bg$Vs$ti0DwOn*gGv()V7IWi)G{EOo@)* zu>#$B)8oUBl;tm7r^bbS_n%su-LVm5yW@g{vwwVY^!1F#rh^!?;|I&DY90?sr?FUU z)&2SdI}1)!mS4Dj*CkOeJU%}~xHBvDR(eq|Y$2N?C$)m zpBq}ybV>x5BjAT2$54HJ)_x7}^I0F`O(HrRj+5tZtbXdBOO|Ehu)nJ)lFAf+{>QD! zIpy)n9bU__0}`Wn=dM2VCK8il!*Dq4-oljI4I(tMk-50WIW1R+hDH$@4Q4VTc^qLx z*on$=Uov?`J+8X3-C!~u+qdiH~?{8Kcj3y^nh9JgbvD7qo z=nRXsZ6ZHkpG;v4vWrCv&>9v20BT6bqET~`1u958Ffrw^Q+TYvw3x{M%ev=7BACn; zz0tJ#u~@B3u1rA$Y_Z*I>ed-d51M=Yy~&x05dZ)dgQC?N0RSpU&!SQClA~1`-M}l) z3!z6YE^x}#n)ZPq-{eV^|yU1bRX*eZ(wZau|o*GdeDe;5+AI z4u=_by{D$eM6~xU-?w6W9>xI@R;s&m%`T|9febQ%5W;3EA#HJ1>fOe6kDbDz24u&@ zAjgDpTyLn10AD{J(p$5WOfXxlE`!tJ!rFS4d;jC&3=9S&M)CjvvkDEDNhOmAAzX%1 zquZ5}c&DM$qii%>T;S}VklO8b4+-J=wGx1q9*GHtqeFv-C#OS$m~#s%CY6GIm^10r zd9?-rkev`085;akeG?~;j_i`F)K5j?ua6v@k}H@r>Y#M`HHySdzz@S>J*4y}KPe@6 zxkxN_WC=3_Ti(J+B#;RBuaEBj?yEzmKPyX%e=~wv49dI;0sw4HjEV~7+GlKJf{IKedUQQ~(S7D(Rc&iWZc=!H5shYeZK2T&dy@%Hs|#$k&8xN7A3i^I?nX_s_}3-5!bskVY@!La@YwWyMX48W zJ(*Fc$wb1E>q&>?^!K;!w}^97qV6?zp^=S-iwm420--QjNFoprs;M9yY_V8vwzY*Z zd|v0^m{OyS2@4sVnBsGT<`z`wahfd_P9XiW@(+KiZ<c^EJ^N$Lq-?=rvx$c% zo4bZSNQ+&$ku{h}JGQ^@W?je3yqf5RhuGMkbUO2hd5axlai7I%a~IIqIaK~Z+LI2+ z{buo$T-h@+b^cmiUUIb7V4RvoS{P@Q5G}xO=~|}H94OB09i4tD83zE+1AOOI+BJuQ z0AWN(U3>4oZK>aW@zK%pe7q;w9=W*PLa2Vec?m+~JqET|ez;Y$mS`+L^)SUP*^$ZmyNcjazV!;66(*}}Zkn5ug%PR9ux7QB?u>)Jg{T^Kr zJPw;qZ5B%a07e+L*=&$bk44nHYV&HSt$)bnY0u$sTz}YJ{j579KK%5t@we!gm*-erub4^#uKA>H|siD z#3QiPMP@f4TkyhTcjYFP<|lC&0e2ciqJfDwvPY&f1mSxNQ?Xdgr8|up-Qtq#*#5!? zt-Uh}wMJ*;GHLm#vED>Nbz>Jw+3vVt4xadv$I6n4NrTBWE}Pq|FXH3*p*|#1IETHq zP#DSkeMON|)^q+^-Nuo1<6RdaW(7T5J}ZZ9~a+ z;(|y2v|v-)0tBt!K8+@LE3a; z5BHqIWdy`_K{t*LQ-Bx8C+W`xrISQaCO^w!k+M9j@cj7jX?Y|`CgZWU-aCq_C`u-G zH*c96pRbum03@Ojc9B6*Re)@v$T!7r7t>7LS_}DD7gmw4O7~S>n5^i}JTABz_Ij|? z{KHeZR@Z3d#6IL%;R1dv^}gQ7KUTD7=1D9TLmS-v23yIAl(7qwZfGr_%n|MWW7>-SM$3r!zv#w|2$ z3v7G|(fITtw$JlLRaH@J zH{;mjA_PGg!vIhdgv(MHYtXzd*Y?`v^I!tE_UqW}NGbqLQHwC_; z=lAcK+1cIi?99gOHP`*QuRHpKvJ58rYjgkrFy&+=)d1j`E_hCaiU{s&nwlHI1CpD# zoCYc?>e{x-PjHFjE~VqH?quoiW$Fq6)U7?--65{#A=798Kyof8DW>7QdYb2D@KTOs zm_B|QtqYZk(lEvyK^2#PK*N4{s&VOMM}EGQ*wmR1>~el?DFP}fC511BkW58Ii70azHI(J0WXNVu)Q{sS!KS3zOB8dN8 ze!Lg|f7kv$7Gd7ymF2Q?hj4AF#ks{owQ_jI`r9!^hA~D;390`)=*^0A4ZV86_|1#K z`83T}4^!+s?Dt8TeuBSTAxfB@xzfc(0>7Wb`9jGzd&pq2d&rY7;`dcxOIk}4W8-(n z``0NkuE{sqf~&(tJuVI+m$y(1dn~J@>cfDVX|H*H-YOx-#N2kK*Rn@hq0RlqQ$qdadCt9H`kf?*)NsXa z&s%<2ke`enL}{|iN+rL`myulogHDBwnW@eH91G@>_K@U~PrV0$a=O0DhaX7$XqJwM zE4>z24T@ufCm*1>(AOCJ?S|r^MoZ>za|MRwJeLpfoB#lDMmPB%u%+`Xcq`)$Ew0Z7HK`b% zzKds3svngT!#;0qYT1!vU}LH*8em{x!4<_`O3X#|b%y_#`*2bW3EIWP$e7&dj2MT) z>K;Ny#2R?9fuy3D&ga;9KY5P&PIFXicLn9r-}9n2`8o^WbbMyV?NwDZo5*6H8Ja)@ zejlCYRKD>dc}+s^9Y4D?BjS5V3cQyyD(tRD0N4X>QKfvs;p>mjtyWr@4RsCm{qFA} z$>a}0oP8d3OO!Y9k%6Jy2s;1=udF64ACL}FLE2=!dHRH@mfolA;kDn>Y=p)ebt{pq zyludyc+IlqU=$@@Xs=VZXD;?M_T6D6{7yfkszlYMAls8zfHjjV=on>l>oMvJ63-Yw@hm-{seh+$UT|)EiL`P@#%2F$f#_`@~SFB+Tpuh)~$|9r$ba- zB4W0D-b+wJm58k}^tAQ{P4n+nRB|*f?i0l(4W()o0EMRO&*nsX;w`xi8a8=peRZ8u+X7hTU%aZ)E+QCX1G>vF;+W{ zWX)~sB9&E>K?lo|dg6kVLIB|P+ap>ilz;qe`=^DuZNi*+knyVE>a8fc9U|)g(aG2U z=w$Z4I=NQd7t*bLMk1M8DEq~^hOq3&Fre^F!jGz!md}wl$0V;EMFK}YXpkPA#O0-= zraPF{zkW^P6XbAuO2zhzXGDaF?_fJN>ZpAc1HNu;=j7$(9I~Y#Bhi?kiw%@lRV>U* zlOFPZYiJj|dldT3_J;akw^k;Rru56g{E|qNe@0q9@4$@IKZ%Iid+G{t&3rfX{x zq^Fa&|6Xd;W)tuiQ&S4qm{B;mREQDWNa%;w3FPiCW=xkKpfNfbo}M5_7|+zc^ft%C zjF0b7p-`E6`k<-rwhhCKC^8czGfalsIyzKTRSZ2TUSVcu>*LLB25z~ck+-+B5Zj+O zou5?fTySoRDe-uySha%o6*w|gSyl1wrF%Lb0g5Vmy&fHNWYo81E-~$WXN&re4uZd~W?w%bA*nph zjPQ{#EAvJ*`{Mjx_xMAxn`5lz&$H*q6uYpz?tgy|Eg#@V&r&ncDl4v$qDp48xB$%6 z70k_n;EZfN7b#-am+6@|6uS>+60ejKH!5)!pWy&z_DNq)G!f#S6`+(Do;`Aql)Mek zJJLj;|A>NX#W2g5dmLWzY}O~9w8dy-Y#>g&&;q(=fYp|&uY(Hk*KHpR%;OV8G{}h} z*$9eovcj~Msd;lSf#rHvkJZSkoI>n6rU`P<`oyxuO;MFTrxV`kH&kcPgL=LoJW2tb zRpIDp6kud@q@}5$U9VX%`*k>^bQx~I%13+IB_|sCd)a!VD@K0&%EKMh^!?335D@{# z_>$99)~=b}r}mhmXon8Zf_LvGR!V!i5zvx05B!CFpj46LrE29bWqkek1uNfj*d86{ zcgA4_-V*`SwTWkE3&;ItMa(baF~${+Nj?qTqZqK#|Guh+!LUZU_zhTt)HQ}Jp$>z8wX(h+No4<>C(huY<x{GaWR=#6U5a9!$8rf%+8p?I$y;$U4Q1?1pmwlDs}EaXdAO@W7|b2>whY4WFoEwex^5z*xc~smSbW(jjR5RBZA?!uGbk&E zkcTt>DIk7LrmCa2dN}_cW8B7k=+MSadtq_Y8$YBvqhzMs%JZa)$p6^qFVm*ZMhg@H zqs1{S(MwzrpX&MOs&Iz9j++EC$$*|n6ls* zso5?*BqAdNNx!)f5TTT_lHtUl68)HiafxHz=SS2{<1P~7ksn@LOwj%LitqU~hm&o> zcj@U{c}3}60PJ~zY5zi+rdbN!)E4?q{3W^_PP7k+EdSD)+IBXfnzrf+SzZ3h^ws6| zBSMFlKl8+c4J@%_>e4@-)e)PjUmM=yb`V`0N|tvJSHNIIQ=+SX@8);m&1s#LSni5p8WX4L$F^Ti zG5l0pTyw{J8Q`c|D7o56J7-DT6V&6n)WXwq%FN8tdy*($DyuMqoiD`YHMja0l=sKo zg>2`_W;sQ=v`ap)@uoJF97|bgaVEt%1IYhrlmm)G zN^WUmUsO}{woIBbr21=h+bg_I5DOSL&AqDi2wz{5>MP3X)ahqFUEOSk0>J^tyoLha z?rRsCT@EJ#%=c0*Qg^-a`hTV4)RIykeEHv(Z}Gc8XRmmeTrn`_8%r5f zAj#MO|54C1!^!IA%o;+*Zq*B#@hkZKWx8K4#uvq5Erpg=FG<=VzvZB;fjV9ul|Xon z0*0ty7K0>KO0y6Djulc+<@4lzdSmX@gTMKqg0Hjr*}&3?^NX927CF4r3?bXhjUTd* zE|{IIMOUVZ<^t~VId%17e%RaM=-TSLDUe}KPM6F6CC@Q2mi?wi8Ao;`&}%mjR-OZR z=Q|D_k9Br7VWnVp0ryCUD+>S#TPjQh)B6|03)zyTRH+PaP4r?RPV82r2L!ElJ;5BT zQ~k>|R>HN9p`t20FYR#tc+WWXw{2#iFLh22;BF#AO0ArX2HzQBv=P1{3uk$C;40>> zhS(y~+SITE!b7-vnQ=Ga`sU0J(Wnp#WP!VkMJ*)5F5O+aGycFZuuT~q@h8u5Vygan z3xfXNot=;S8B7Ng4v^-qKzg$$*0&tnO9YiwiW6#$oTMl^K5gw*7FMyHOCa9eQ0Kp8 zVkBF=9gjfrqk&x2Zqi8&4I=_Nrw(5I{dJ+$%!{QRt)j@Jv{GPYMakAT39UFiCCh4C z;eJg3BqhNz#-S|@dZnd@w~tHNZ5VE`=)3Bv9edY;rFgJCDAWQ0;P-O%+R21e@{=ZT z8^*9oVf*5zq-XXAX~`Lxmd|}Y_2=Re=4CZ7bF$f3Uv1d+UMi!EXnP?)ImTRDvy*dr zc<74O|Do22B~^F)r1McvS!>laSV?kyTm)=MuDEnAcl#VWlqSYjC?cP<)^ z+6B2npUk@10V*Bfy_k)hxerLCMg7qTsO)lh zsPC7^HSmw{1>2GR?Mo{@35pi4rv68i%yCnxxCD&uJtZ`-q1K}CKEtvNAnT^aaZ_hL9k ztQ|jKv!o)DEfXKom%#0weV!fxFhJ33;zIR^mStZS#uE-oH+-+UbUuVOT2MM=&z z+NFJKgq(*~_^DS5dz5fH+M`d3n{bNDixO*Y4}$c6jbv9)uVZDkrtz1lw3&AYPS2s4 z_&FrkR?Pvc7wND!Zgag|%kV!Hqoy-#(ta-pRtU$&#@l{ROU zzdHBq`3Pi%5&SU_D0a4>%Of>ye#Y@C_>!XV#{Y@ zW-3e?rF)D6vCTkJJ-!0hd;(``V%=VGG;i4p2CDMi`k=>gO8PPE{HX;_iNhJaXKNQk zOY;eCGL@-d?M_QgT}NqwN6W*syIxJny#H%!$pmaovp!dPSG86)Gk24IwY5u2#~++W zm8`RjkY=*{vElMpR?H^4xLN6Yt`CT#a)&k>Z* zPDP!2dMhEfW~()7hT1oj7b_=!Qdu|K!pTe--r81FdIgtiG^j9c@d6)d+H>H3*A8D%z_p+)$_QHZPQLu9$`X!#v-R) zI-j)kG;b!W?jwx!bdsY(rnd}fS{fF6``&Hs+dgv}P4(S>7)N~ur3^2Y2Z9--K20!M zG(^P6V{NsE?libY)T17tWP&0trbi+&D?}A`$48-=J+7znlF^KjRLdf?>FR{E0>O*C z8J$&~I_8)~Z>3E5mnzk^PcdA+kqR23t4zNyOhN#bi3lRc-voKYgd7Zo)0SS+DaA$w zMYBw5XlpT6;&onpE9qRH8AF-kDd>(%i1VYRR&uho)q7{ItQ0_&?{{cxvxZ%#NANA6 zthONb^n2pn9eu4KK39vi4KDivyHi6?mMQ$DHtVZ2{6>M?dI{lb#mRx2C%Zs{5oD98 zEc4sTI4*+oQEd(FR<9INXUYQtzt}8HZ!?d+Uh_Q>JT>44lqy=z{t`ztK1wNQ0R zrVV>L+$Dv8nd4@m8vLQT?VdjUe`&31jQ5?}s2)-B=oS_*J*d#If`r!>JU)$op_Qh2 z-*=SPBleRA0d#=nPZj3=j>0egkPCA&?+g4ucI^YnE%L0+#}wxWO1+lKClK3kSgB0i z5Iq#j#3;I)T4N9Z`L|>K5kB3rQAj`u1fqNmYs2-WP&p{6uAUrUAmh?t{n@^ZmD}n= z=9G<^I6Qr_qU9%h}Pa&xjaAK zM5pZWDSlFt?E0CTabI8CW~zKx;|@G2`Eq7(Rr%++sB4>n5)aR&OzhK8{NyCT!U6zT zN>EGuQBcuP*TkHPH|`2R0>1ge=d6<&;|*s)ddu5j>%etyZniuA(@k3ux-m07FIhZW zr8May%8JlN2$++SL8}R24EpmQd50+(VgCx&n$W88mr@@;e?3WkePd%onY}Je@U1v!XN8eS4);~HG_&4X zqY~#7!?`o5nEEYuJrvJi35;39N-UFG^#(XGV+xvS_)rTAA+n%?JO_yV54*yJv~(Y# zmhDA_&l!VhNb&N=#TA31KWVTKk(s1QljV#JUgk_@?Auw>GJ*D>utRhNEB$@_U5-Xg zs3R|S$|-5Yf|wBvuI!>Jt$^g zdxcfyVO^xs#xi_ABk!c3rJeN1ZJBM!^)+Q$I*Rzf=eFk3&<#~}<47yLU_c5|3U-Fe za0$}9NI`78%MjmHLGuXj{PueaJTx$=opty9LaF~VG^8X%_ztY4Ixs$6!2nMRB^FJT z{<*>QtI6`v_a)Y9%PfkRar^g52|*7iYsg~7vqLZb>~$SPQt$~quqbF{UvbTAs27L? z7PYiXiy!GHL;GqDEgVR4a_0}dRWdM?J;lTVBzAFzf1LZoDfsl;nzIye$fzrA3{CWb zPq_(1j=IMC$LBw-A^m3coNlfIO<(|OOf%%y*5=30U%{ESadIN#_agr9-vrKRjEqkY zDg2_hN>4t2(A}ub!FOF;cYP|UP%$gw5W+v?iy2pt`R4QM_{yc8jNc{fMmr#squa{s zbZ%jmE~%8Bt@w^F%%I!Ls%dz7hFk({xlmSC9}@pS;g82!ubB3gb`oEH8KR>{GcaAC zy+KY!s>g`IPktW-3Dp6C97%FN2}4}++4lY}4zlhYL8E15W%*YI4lvDLuX9FQRA8%M z*mZ5Q(ai#a*ZV@iWG4lE#=NGp)lz#=qqU`8PuJn^!;5eq`{>p>oGej)+-*KHd{}~z z6^T9xmz(fS$hcTT%Zvgd5$f-0RiY%%Hu_lRKVQQr|4h7WG>Xn)pvgU^t&qIX6zy)W zaU9=9;_|1j%IWzeNmUy3*ZZ`0gAM!9CoDaB#SES`Q8u(@nn*7Bu0KJhOL_W(=Gxs{ zwjdqX7k2ncWoF){2Xzt*mhfGDI72a~+39gV(Z@$fg&Z3HVj~Fl3}6o$7=H(xNIR^ z!4CS61SV(3MuyGTH=&KgD(fAd)Bq(DpV#?H>awFY0qxM}X@QYll%Xf=9xxiFWH%w> z-PBb#rg)$`T-=Q;Sba->kg>RUi;ti{t=7Ol%yUnRi<}H>URyiU=(0 z>do4yZLzlg8g=d6b`?l0q8h>?BPWKVuayjtG-C>T*!QN{MDjX<9pW9=FI!8;7TIH3 zt?``^G3o3x(V+vQfjO*}Z+7gIA@PBE?@K>`X}8{k1xILc^{4*J7WJ@nNXOg8hbelmia#XOQkx$9;y9y zFe)_(8ymJLB6{+&p+)UOeBA=ZBV*jQ|I~`$eB3bh8-bmceU4edK$3H(-(=pZZ129H*Ev zJJ&)g@c!iB6V6un(S5r@Z|jWPeFK@K+H0=}#cxtb2zUqp03gXoiz@>FG(Gql2M!WE>cVYdflp9QqB5#*aB!>J z3R~b?RA&iIXBB%hXE!59Q$WSS#o5`^(Kv7#9so!H8F3L+_my)!H-5E`55HWeUtQd2 zq@)sNvYE1xe>cQbGVxY+XZ4E$1W%Ge3nEf0IZ$QA#qT$6b~d~wFXOCr*Sszy?kemL zpLqP%yar!7r%pRJYMy+1kfELB0b~N6A6vfzpkV*Ic>RK^hBwwmh+d%l1JXg<*y-~( zXaYY{Z*OTHG|<+b_)vsD6n-n-z#Y$|m*1h6Rs%ggu_-~_Y9Zqd#BSsN&GO%LL(EVK zyZUedWPrcQFKR^~U5@G-EP)?>sj8t>BrLZ=gynJKgUb|SkHXp-KQmM3gUAf0 zs3cxTQPTVs_vt%4s}UgVVk<^m9YVrl&(u47RVQ>23ER{kghK^QT=n@Y$ne4q*?xt#Qv$gII{Q@0daG0$%JSw0g4fqv;k(dEBKZVQXC_v1ByANs`ig&eiz&QP~ot z$-X?6aEeoWIdX;(qNX2~xp33rS`)r;l3jQw@0OrUseHpgpnY%Re#ywuN9pp;y|S79 zk)FER{HdJoT81`5>f7`FUg}ln;kqhpemcxsdse9`^f-Txoo{w8)aH`NTClrkFG4jy@KV%GF;zj}N>_OllYGutOc3W}@uoXq-jc712+3_vj6Yf(;> zo6exZ+QyT?u3D+7?>w#}6Q=5Bc6A+Yt2$ZiK65)-#u5@Cg1(a=58B$i(!@its%#<4 z^?KSfuRyWg`=6bk0Fopb@NQg2(QCiIJvqnj*=lPQ=OH=7bd7B$|LLapo}Jp?5XOS| z-*cf=ydk2I^3p<1`$`_Bj8h$k(~2VM-=_|EVLb6hEP2Bw*Jzn+(e z9D#OSps>G)H~$xc=(A0QJ@(_Jx!^t)@#4VJQmzN%O3uonKf615hGr(vhp-#{CSv%$u%7Ib{B%zBpH-X;V>ET<4BZ)g? z=1gn5Not+^&8eyCmoGzUcW!srmXj5POA7ASx9b-{g5H^JufZ`Kb9-G1wo5)|l5*cc zXrT`T535bRGcS!d<+eElKT#2x`nI|RrLW&_HWEK^5%wV6aw6WsQu>Y_J@MeEFEcmS zXrf|aJw9ZAmDe-2^2`aTL;l&j*5q)%S0`B_88q9y&%FHO9(LvHTYk^XFrHo^DCd_1 zHQgVI5eUFy;!npE#~v2fh_oU6Yo_OQ;Z;DJL?0bO4Evy2RFva=FtJqmhz!@V=D|m^ zh676}$OjE_Jnn^*fuh!XCYf*Xen3KB#(VeXetzKdFYa#y)`(7S9TDPvA?#Vo$~_7h zb#Gs_o70B4*xDv;&@j3pa4v)YVq!G@p;a z%n~S~5C>1yU~XxBg16}_P0v_!?7AIz_e-A2mWbQ>p-Za9%|6@sd+B_`*UXbhs?ZfT z;%>7yOl(X%+mepUy{cvUILs9FHe!8=J!87Sk6oJ85Lm5Mn_s)mAo3GENBl6j&m?dl z*Aktf12>^pY<_I+v$ZWA*S%>%V;n7Z%Ca8~o9qLt^yz^=6n1=&|q?Tk}i+b$9^29^ygoCJW6IoEUJkoBvD;AaIM zm*d`U#v3z*hO?g;JTT2!gLijb-T?$W(#~$w|Jtgw#NeW~SYl63{KH_4izuD<;AE5_ zKu)R4D;Krg+(Ax<>1HKYZBBoVXN~Ob&6G$100T3yh~m$DinKK({6Cr`cp^bV5UB69 zgV=nEZ7SD1IFyXro(%$6Sh!mAfQEylK|nxIjGe6`BTvYq)*ToK%rFMSoe@rf@(2ck zjIyY>$Twy15ieV!JgiEmCGB5O!eGr^_wtYydQ$pxtz~WOGGo8-oW8{!aX~f5SSWG| zyU}#wmeOhPgQmuCc?~|%CIJKV^Iy^$ip}w27-`T9NegKZBm2t&lJ8t;;aj>ftcDWp zUepE(A@)T}_EzW0zMPHs$`2^{kJA^5KN8?bW>qCsr*7j8cYqs1$HHuXy3FY-xQ2Jt zq2g=(5+3ZwZ1+o1dbv&F#0sI(>Hb_7jPY=QqEh0-B>i@h+8LFOX8K9pWt-3CJcXCW z!U2n)f;Bxk8C^ndN$s6G_QUQi47`I8R&FbZk7x;WSt=#$t2A&S-{4i8%<5iapV*n( z2@AxhBpmHWAdR=T{AHF?qqjiskAV<3TEY~LBCmel?mN0a#rHAmgDUP z<0~{q(nD*Rwe)VuE|k>h%}G<1o4EHnsg_?_R>MuUqmBVlr_|?Zo%ZH-zTo0Do zPc>XeN5h@4$W$0C8nF~=!cx>+G~Rv4C@Def%@bd~prP5=SXEb-T`pxlrH}7g2vO1U zo|b%!CDxZ4o7*)t|FyLpue&U+E?3l7f&yrm%2ZX<(gph4Yg>0FUQBWxzLQCRCagI5 zu+o^i5!95)s$*aGloSy1dFuY&FqE}4S092a5>M!j&}s;aa2 z=Ytgc*D)AnzZv*yYU=(IX!&zD-4r(UeaWfD6qD0V>SOFtf64fFAtJP;h0VPm0pSyq z$6p+=SoN`+tH(rF`ks<%hqr2a_+0j=v2q2yc4kJ}lH&Y7H*I)$Sy5jrG_$&6we@^d z<~ovHEoqj-t;@=+&z7{ckHGND)91N2O}i`4b^gz6F%L&TRXy1;-KNJHEH6=AUrq!l>Z%u)lbu?aYpH7A zU5HUH3V5J-J!XG2-?6(i!CqNY;q~4e8XvP$p@)AJW;S5UnofnZ_+O;0P5kpTzfwL)WJ1_3~#a$X~J zOtffG5vhPccS&SEP3to=J2&>ksOaUowUf|6QwQ<8TRJQX8{~z{VmLhf@cF9TSvJe* zkX{YpNx2@zNG4@c+Bn`K)ekRJ!?jYQ2$>A~?<%-*6g9PL0~2G+B!N?I6jXGo3;n$p z>?bE#3rrb^zJe@82AiRXl0j>sh|S|?I^6E%N|v;BjQD0V$I)OWsrU3C^W4JkgudRl z6FJZH>v97n4HbL2*}<*`gC>;j58LVggeMdjCfU08b9-?9`xsT$Yaem;(>HF=IPW0~+bb!A&g}bhgvdX6O!SBdjY)FP^7u6Kgr^Qa3&P8*X4fBm% zVlcYawA5(k58^f-@P0ClrH=~<9?(#oXU&jy)sOgPKb?7wZCX4)rHO=Hl;{`UHD~@@ zfnwK~JuD`Uh^Kh?AYRcP`(3bdZUgkoSS8U`?wRfmq;E70AZsne85GNin{;{v)RL^P zG}h)vB&XBO-ZYlFe}X!fW!+xbyr-)sf4TlcCT9Tk zqMxxGq8+3v9{lMp6p?{}&Y)@hB6>(~Ly$X=tK}Mp$Y(5oB}4j~ujSC@=m`vjpdis7 z6SA1bRDD9SFNW5i-(Gs{`R=DPbzX@jTwdi7UCeERR)W+BX8y`vYEdaD>07-X87;o{ zw@R$Qi9`M!qd7s;i5g)^cO zV0d4ya8FB_eQdkj>WxoA56Ddxb>{ejHMrMC`G*|vzjmBkY~gGA8mUA%fXqjPI1|!i z-qhuTt5sSCKQSzn#|#j*O$FNe0e+5%ER&GM6D7D z2L&6(^{_ihXyy(L2~5wOT<;n56bK5+wPQUoE#FOZE{ZJ%>JJj~JFS9m zHdpi0YJ^+_oX>rSS93JfR2NT= zNKrVf)N%s|%uu!;;`&(rsKh!X`TF3q;UGEUvn}m3qO5O8#Z>Elrzc}@zw3#gqSYd# zT1T~2qeACB_iZUHa$LdqjO0T3j`?5w0g^#DVJs|0l@7oArSBIVCN8zmt0;T&qJ{l2 zF^j@ZEEQ=@*muF?z*OQmTetfF$!?Q>YD%cFHTYB~-Bjp6=?XuG9`|u(w}r{}M)2JG zB3>oz1p=tewQ)WE)6SkoBpn%bbXqFW`Nlt}o9kB`-Rt$1u;;TY;G^df`;wAkQcNMy zF~HfXW!kgRt<7e=HoFS@Gi{ZS-zCc@(nZJc$jMCF;x$6xYd>V16^imjPuCpxGky8Y zpaT72853h@ibdIccDLP+t9qrD^Np`DpH$dQDS0amKC($oi&pQ~ueRXSF#H?OE;1@k zK%;4LBQuf`;969Z2rgroZ-S1F%>8;evuLl<=i|}y1qK2*0L6M?I;V0nI%cgFGx7;w z?iGWH!OD#`r`pq6r!wH*->5Vwxia)^bI#@vQwdGPh%}(%<2){+^>u4uoAw+H(uRZU z@7{hfsKFhv#N-8a$F=4xdk5c5A9@3HOUbAsuePmcGrEYlQIV$oL=!vPLIB4X3{7J! zb43*M$4Xq@Qb*@vy;;!^HX zHi~!&Bt*%1c-rFq?p0A-%_07Ef4kw~XEZeh4TE%-i-XQ>Qqn`G+lnUsd*I)KcPnffJdO`@z6z)5H=~q;XkIjii=h>U1-vRX*dsl!u=4 z09!^62{>MVwcm(qJ#8ZABsWmgy71{+yd4kBLDKKRH{+L65&4$rmh8L|S3@Nv{OS3S z=6Eda;vmqUr1blkUO%*=#^$|_>=z!M-`M;(3W4d=v9nb}eJ3ZVfUrJMCJTh%kNEr& zgp}k0vC!XiP>=F2R8(vop{fBLd_j^>ZWk=%WT?RJz)!(w*A%q(=4K8Qr+`zY z)j@fTZQiW&*i?HNIUqcEe^+Udqq2Q}X8f2dhNVj4%92}IQxSN;!qt+P>f7PwmA`EE z18WC$>t8=Qu(>QJ^-*$%?C{N}G6WHT=@nT&tW*|LZ}aPgHH)hb6iaFQ0_61~1EDav zDVR1ST$|f`{MXW%1hZ$r-wTCjuQgLgJ2x0y`h(f!bS<sQ=3__)1;_|5D->sE&>YLTA8O6Nbx=BSCQ5W zqE7zRu`6#wHGbhlheuqnFg?~bR$QEaW947VD>zB$e~C+IwPWWiUvw@i$ZWuFbewmj z87Dg}i)NK2F}CA+tyR_=*c%A=qNymUr5UIuB#%D5?2y(70>H)}pOIm@v!28g{I?iJ zYePr_QG0)HbAfaTSKQ~!pBqh)1c%D3m};t)b!LFDclr1Oq*4cKTQMf&`mR4b!U~l2 z5_Zz7orXh99VDQAgb2X~-(h$5y1Y9u5F8U~Nx0IrHrL8nV4$Yg^L>5fVLR;`k(Ssm zb|V}j?m0PmP~Ne{9DgHe)gt*BF7DZz>#uRyw_iU}xktYr z{S&JI!(_rtZY>|->uc9#@y`-+=kL3-V2sS&H~g8N+*Rb`Tr=7RLq)HhhdVHY^81nHE0N_Fr{k@6r~nBWg`sKn z$B{>LLsnXM>`7VISem_q!r0ar<*u|Cyiz`ucGNw|e$2O7b=#X@qjb(DJYWkObsHO% zl$_Fa%6#aR^A}HZ#!n4pZ6)KmgS!P}+hNGJ;hwOo`Rfx`3$dhL+rQ`(7)vWVfz!Lv z34urbM+Sc1qPlO>V?64Qs1P(Mvejj_h9p9jnG?mKHO)N-#LxU> zXaMT{K|^KqpZd1lOTkzefZzT3qIwaT@{{F4dK002G-2#E?b|cE2@h8|s|m|=nB7&O z_+ z0LH#-VtG|*ef5I!5`9%omYUi?SyHZtl09;m%R80Chk%Dn3&w(iz?c}0`)DaKa(PwN z(z?2~3h^DQlsC_p@{;O*xBFq}RiO@iq&2mb-2tw9YhA=m7F8&>C_XgSJ+0EJ1Y`N4 zis8NfxNwVQeO1mlA`ltDx9$$&2pL+cOpmWIqocS6Xc*Jl?c%WbmPLP5k5ghT|5tIv zkg%+(f;3@+fsJb+f&fUqz$4FsG-SG?A#N7BpE{msdK~3%Nd< zNt$(UvoLCVz0-3Ex=bbpRc;z3M7TlqA5IZX%|gk@PJ&zyQiqOXpm8QA8IfbcFEts% z9GJ1PH6~g=cp|56LC_-LpR8)a3~JNp@FatatV=aKljF8au_{x&ZWH?kjkDr9eMrst zY*HnIidDeH!36~b+MQv3`2qk8x_+VN2}wUcbf-pXN&Bu8*8{+s2}YV4Y}lX+fgVdg z==iIvEC}|}dRCtpf^NXKet94L)Qpet(z3}3Isi~;#Jl)skNPr+Lfpfsxv6eR_bRXa zJ3hdPP_idh=E-AffD?&^K~ZUd)On~XtdctNCE1~mJ>K-~^t5!cv56hDqV{6SZrA5#_ui@@q>=MDGZ}~sJpZr5N znWNE^t;cRb-!oCHTZ>z6;B6%a03}w78>*du=(iv_N&Sww)Ja1eT#NR~MkwIOs*H|a znzN-5JkH{d&zQAqpU5J`aKz^CP&0qe?F{E+pA*~y87S-SZDp)7yl1fc+(K6voj~mw zLCTT+t|RVPO2a^COIhRy)z5D%I9LcOov1ihFz}ep3H_p`l>8pQ5F;Fs zc=`*S7$jII;6eb--Y<&+ToT8XFXPA}iDA}noj%MhUebObU908^DCy~d>3%9!&3c|BBQ~K@1%tmA`ttOMTd3U6=^kI$=HL772e90j*%qxw6zOFVU6O}jx zDhMkI3K)-WG?1ji!qUjVvwT_e+^%=jHnE2c0oQeYG8;M@U#EuIr}1ye&VpwDmf7Z? zj;<8#jSKK$ZCcc<4gaqw697y#Hlcnga&)=*6D$$@>5M944KeyLAhA~C*4~N&`elrd zfes6GCzIZfpad(f4YWf{EC&3J#5I^t=_tiz!=)|5K#WqVZx~HUvm2$wI6f%ue52qN zt`pI!{s$Ih=pU9Qd-HG`s;eXG&#K=x8nB5C<77IcWQ>YaAOPC3<=yjUeq;|mVTtP1 zri{(cYr5DHo$ol;WTBY5BV&jB9tU1_b@gNhUo|0#CA7c&AhK^UT_|`%glWjj%g*&G zLs2ELwfu4CIgE9%X3k9JOFV(xKUkYTV7dCUsVbZmf7z|`>U+a`Jl7o0n^m=Ve9w& zwd!KdmNgHYJI;pRB|Zy0m#QTJC8ec`dwdLNxk(1)6wG%wIiI0o8r5lK&~kpc+@b=C zTI-67)0Pfq`^n`r!&e^{o_|@`JKTq0hDsl3E8E*6uM?j_em;@8sCm#k32gG^Gku!{ zrvib4=HNABN0ty#8_RwAXbpXs$qx3+EI%Cj1=T{*UO5pFD+cgz~Vk$JS6R}5;-I7M*_Df6gV>z zGa1PkN;#!*fm-i#c%NC*=aR9$!WAs#EF5sFB(cZ)G6izK1cB9mhU8;3Q(#z4NsCWh zCuMi$EW5x#U9xgq?kub^!Z@bq=2{pI+I^$a!HHm~`*X)5(^&8*vE+5XR-ck4Tl9G0^ zmuCXkbmT=yptUkPv}>zD;|S{Qv|NVNZU4ARSA_t1x8cJfB1?!e$%)S2pJ*BNA?~X# z_X89_LOqXxQC424Z=0tF50qB_l8$g0ZAn!ZJu~N}FDwAdC*Az001!xmQxS**?irAr zRiA(ZO~GK$nIThLY2mMP;~}=FXg|2ke35q9LM;E6xcOChrOE&RapgZRfJe5Hji{%OSQ-f{S}5i+709}Dq@n!!qv)_t*wro~x+p#T z#{+!qnkO6}EjP)AUp=q%RYOF*pf$Q?3zZu$v_}RKa4w1%jA09ZpKkNED4cDqXfY_S z7>VPW|5^7+lOSE&a$H{D6EO| zokiG+uDs%Nixab9Tr3(~UUA!v-=LS+IA7dHZRHW!;|M#BVQ<+{bT+ z3c5k*#B_V#K>$B~+tQYW&71PbjcrWUS9TuE9?JcHng;*n02*425K}f^ej%9`B8&I97+fJcUIA%52LxNmjvp2hP!*Ojp?PRc*Ossc{x3iQ85a4J>ASb zc{W3WHCgZZW7zT;yF9K_+kPGM;{&z-1;c+bR zzJq)h)a^%kmi&lRzHR_`p&L-EI-Svn-Xhsz)(Z04gUTTQbg5=Wsi#ERe9qe0xd{2x zlrPo`IEoLe`U5qvkSz3t`v)o6Je^d(FG=W;m6X*nUta~1BKQ$w?PgEgPmFpGOB9;M z&y`MIjHXYouQLT!u`9|(YYg^T#HG&dUSQEv$6Nlj)lT%ODv0WujEc`{AsfF!bzrDF zYJ=|fli2C>zHW_3N7=8l==kInG#~h@NG+HhVhwpq?|U#8ZfpP7O6j>n;sE_r($^Q; zyZjuDQ+)B93WZx#!r~V?AHd#1>W`p25~x~VDxJ0r1~&z`HWy(nrSgyd<4%QXIH(Dz z)dLg)5@u>fJE3L7P*PvA6jcP7cv&noES72fR@*8capx~%T@(r7H|DY1=|F~wt^2}GbK@~E(U}qp$4Kq|i;# zFY=8I#8??e9GDoSBOxgnY<>@WeBE&Q7-4bfTU|DNohTR>mZ+$iSID`sDB2pp*?DD> zl|87iAU++Zs2tBbT&%BO$tj8}A>|(uBa_HbG+2#;{saU7e+Ec$K9iNxDb>(t3(&pg zPp2|M$I@mZTUhs3s5`tJZLbtJGtr-GHq`2>uV`f)S81E<*8iQ{7_>?KSv)4&;ZTU^ zso!K<)gQ8JjdaJkS2VyF?cxfWLO}Ewi@fumVq93X{LHBE)9~+&4jFHoX+w#&RXKeu zKBvtmOTd%3;nnJE0Rk7qwMlo^yza{kLj4!?%Fr4;t5c^6rxi&R zr_-qL;PXj#F$2HKszYl|{?OZ9UK7t%lEL`iFDpJLV8nZVRa=XyMQH~?*}q&6+U1QX zmyq=Ze#KgH_pk{ua(@uX&9Po(NyY<)0_>oJ!KRY+tF(|=qDAYv;_083E@9pLI=Z1_sIfpskf~#Ip=CMiwoFD zRotuscCE;z}0sPLI&-0_Dq*ux(P^aE58U8-+HeveJGiqWqyR2!5!b5TWgH=eju+< z#JDrjQ6;Ak)@pGcjq%#{_I)p7C&~`r=p4{(tbL)!=O)@XAKd!LI-+1p=S})Ok`!QN z-8+(mm=56LiL#r!A`q*ngpW}0`-HAt8OweZb2{uxO|>l`Ve#kf4-h5+T#%&YG24Fx zKtLD@1^J)*W`iawONEPz^XIMMMn;Cym@QPu6%~QdP-t{?--8_pF)@p6$iPi`WR6a6 zfugdudDG&bL-QoS?^j&oobG2ul|>7q%2{#ir{wE(qxv?>xG95+=fgp;HEQUA0TUgK zfuRf$(bwt4m*R9f$Jn=nd*a{Cn)aqNeN8EN*{Ii>{?0f|{T5rr)^*!VPSJ7fvQ|1< zX{VBbM=Q+7+kE=wQD+_SStIvsW9{BHa=J7Q5yoj^XCzBw6&oWZx9$3BBx;E%Rejhi zY~^`AX^#k3HeihvZslTEM#PG<#Z?VGj!7D*q;2gd2~0{;wICR}(XsHPks_0Ekg$)i z(D8?W9?f&etpz@^iong+;I~{WhWas@YSd_jga8s@3KA#`yh=%o0m`ur#J}1sclZ(E zx6a>EdA)ret?~|S1u&mN_mK@g(b%d!r&EcMvW8ixIyhVUhE=#jDl6eHIWbqX;=ep_ zf{WB#K_1dcQ#zuK!g|Z0Pggyk)!$lkxYqK|Kg)$$-;Gld-@&=#r#%ZvLVq?UBn$Yh zZS$rPAnjk1v#hD5a_BjXEix@scF5v=ACDd(w^n=V$eazsCHGS{J`p;#a7*an?tE>e zv{Jmdm~_z=TBgi5-%#IG`%#3EXzFwEP!s|77sjv3lM68A^yLqR`&+)i3K|5bQ$Oz8z$mseYVG zIaPRjzYLM3tkGek+_80(R1erJwzV3qe~;5QxJ4qeET?BC_a!HJbu0YO(`3XN8Y`0a zmvbfp;Y$IdXHYrMQ;gz|y$H8?mU+AwFh>QIi)|5PrO-#3y2Ox!S3S$P9qz(fhz~`xr4P9k(cBzPp{t zFEPX#2gGhtzHV#Q85ShBf;T=jNK#_g-izHX^?@> zT)Gd6F;>F#Im*P;MxVyBtTX71(EogBlR*DlSUZA*paVuUN65QRLo9DgX zJHB!M?CkR+XY6FJxz=2BuBeY{@~<(-F#rH~t*9WQ0RYIH@I5ISB7D`}koW@sKys5( z)Ivi;TUt|Jg+JoD%j&snf~?)W%w4SjO6%eCsX)hl;O^L+jnZaNCjUw&cVpy zGgpNf5`W*N^(2@`;g=WWBtoJ{K_1m;oxztTfEMD`;HhuZ?oi%-v&e#*0Z=;sK|uZg z7i!@u&y)G&lzalp+l6%yNb7@8*H>}BO|%2SRa#BdOkvA$ZVv1gBMgr` zD*v@0By7Re|6LwwgEA?)EVxCm2PWk)&ACXy1O1dwtAEA`klVOsbbLCCRk#bFVw?+xT>Mni@ySsm?jana){oL6^#@o-IKUs2&V zOFtt3f6Qd4-l3UIQTk3H{6DA3`)%+<{7S-A0e2vMZ}+E95WoT=ii=|Wzpvo@IB~6Q z9ALDd-W01!HAVuuBmKtm@B=+p&GhPk{HI}i1`&gDw}5%-xs*ZFNhgLJ-}6X15GF(WTOrbOVoM!qc1O+v`PnEgCf|!V zuc6${OeOTxnmFWH>XVc3dVdFx)XjS0hkdM0@ylIgXe*|Jk%$!^c_3kZ;dcc9 z2s3CWbdnCvy6mU|&eXm6-enqHMEd|iw;vNJ3}4Bo;|_LiO)sx~ISBvOG#D7@wN~;z zUF2Jx|IIPqFVQcF$8iiH;_Z9i%Vuq{_R&c&q+8xak{NuG7stY zi}S9Bk{oRkmUq@!ls`tI0RHv?!Rl62)4(TE!eKL>H$2*VKSU2w?tGiCv0`c3-R0S| zRdf+!?skuCtYJRY1r=4l&4*rf8yVyfQ|=vKeatR zJ|Lx(I*x8@7EV5 z^m|}?bJ^VadEo7Vt~Hc0UyrrbMB5ZPZ(pw@Tstk#)Vl+Mat9hw6J!(7sJ_7yMhzcd zW`U%AUv&JpzuA;k*Si4Is;~1mbfyA=dMvOzuFemY?+TN3hs$WBTfd;c^lvGSG+Bw)KLq;_m`05>vUs)xSmt!lw8bn;?wR+gq2e3n zHaHLXMR1+XS)u*weOXeE=!d_Rn%+*ok6Y}5dbg;ioysOd5Ex0VAOfzr27xqO$o(r>p3 z5(x3aT&(vw`8HpY;zo0;J@zE;X~H>d^6O^X^Ma9pj}BGE7*B%gWkzY_g43pP;-B{>_D|f(xF@`f7|y+nSJxutds^_5^4<5b9~iXwcDB3I^YX9PA5lPs z-G{jZq;has(bipT4ChtIuR5>4lsh>FzDy{xVYhy8TY&%Vf#QC{eA7ru$u0G*rgS;# zp@A>kt#2k75Mp9+2o4P(Z&HRA?@@MEHuk#lZ@nRHu5LnD>P%sGXC3YCpEX!V)V>AS z9o*mGK=>BjjFj!k+4qBj20Gilz@O#g@93ixLTPtzUc}r@)vTG!sqI?7n9EM~x`(HE z2E^RD3iRfoG*YqbWZNW$Vfz)BUs05j6glT1O)fF?+GIDDMXQP4Cs;%A#V5EpwxO{$3XXB*>#wWAHCJ zY?*x6^B*)$l!jq)*|M-n_?7&l01^nfQ)=6A!Vqzh`9eW>_Lec;HMStHEqL~~9yOKI z8OCZgqZ=7{h}ieQr(AEevhNlnna4xPtTC<6kw$plM~&?7*x}HhxsyuQs26%m9MLDL z5X>c%)c2ESzFv_b(SRtEQ40w@y+{sKte(HmmXGMi?EAVKH}3dTEN2!*)lojy*++HN;AVt3dv{a;4aZnv^$PH4txAV2!UZ!=DO>>Y;_ zA9?EKcGhK%s30=GzdnBT7d_@%#>rp)-Jv`4&nms^_UJjQC@$cCaZl0eR>FW!9D)FR zF--5zn~ozUb~H|G0ekcd+|3c6cd)!ck8Jg5XiG@HXW53lVwH;Ljm_tVl0&dnNnj zYEl+Pdv~a&9KpVAfEg~x-q7;~;@b$14Ag5r_JZO`bk{jR)Jt}p;Ni!We&qs96J=`C zPoQ-i`E2>+aX4l9uON+=r_bjd8Zj|ZM%{2!=fG;li}K<*9$U+U9)j)4X$sDie?!R` z%7w4pdERnQ1&KN{YrrhHzVzhPhT*Tk@L@sM`&GL^DT zSTR*Z!<;&}f)`QO!qQW<_pwTfSUDGj$tngp8S|6k#U7bF8U`)*V?@cS3Rd|9>y=}s z93b2GqrR<0gu^`~mDsWBe;+=eeN|yfGAMpm;%I-k8-?CL$Go6e4M!evjA^dM%gJ60 z1W6h{{qgUW21@eb-f#nmYTwp)+WhxS+uxdh~oe+2Y+1`xTIu@8VM#NsS$0yCleJdU1uA1mq>F^p7Q^bVMH- z(UZO(Hf_ij-p|4U;P(ui32N#E&BqoVQxzcqGWhthzfl3f$$C2U>e8W^s0z$WNy6SR zTIc)dlL^7Xz#qOaBE=j}*>hz-F)?Z#J^q}{4B;$J=w_FCM*5474U9wJI;+s=AcfI# zl+E2_FlI71020yzzX!P_I0OTH`8#)=M7z235+XHu`g&aPwa7>Bt>QP3iAawIJS`aH zL;XA}etLPUuqEd-xe^R*vZ}x#tVx&D>9xDr**T)g;S8kcyl%BrD*n!I(@&;S6e5p_ zsYHP?z#!=tuUK-Plr*PgAF74#GBVsGTb&&*j4AQ6KrXG*g7g#i}W+=(!VO zgh}Qji`+ooijG+epWDXA-4I6kpi(}mszkIco?RScQ10-B#@m_y#?=bd zS+x7f$6D92|F(Z~g;m#{uXDcby49ia>m3x1#tZ7uL^{$1!x|>jn!`7!-@G4bb%>MX zRHM+SNtBq1O}mukDACr~N}0`Ls2 z0bL+6@<_S<({r+}+!~+OUZAy1)@pqK8-C*PkmAR7Fi00sU_9U@h#+J&ax6EA!Ub_3%e zkj8;O$da`JU*HHp9D6SLQ7!qrr8-@|*unGqRWMrM^U*!=$ZJF00%F}c@TdWQv}8U# z;w=qC1av1?JO?Xvtn+)kJ(~Ob(gxn$j6Cuc>yA`i=E zWQ$C|Co-61sCy(%mzXdxtUp&+vcf#jB&gJ$jT>LFvndPb3b7cVs5XWpk~BTRyQNCe zyR9+vpnS1kb#Lee%oGT%g~MM`&pn7OnZ5cC*#KB8rzVCkYh9LEjL*hnUCMmwOa?bly=xU`eJyM_kA<1U8QU#3&0J-?I(48TAXlXDD;W#2s33T2eXvVY_$iXo=6)jW$qLN1IpR`;6T^`5E4B(8 zd9vl(3LV23PosrSDmU85y5%|$sRBV!Z^z zp!aE)n>g`Lix>P z``bg=|5JWZCpVRoM6)B5PpXppNWL(a>VUX7^J?Ew$HRt)l_JMR6%YVQ5ns<-&B+g3 zS~F7}plSQ|F6Yzv=B{R8b+hpP*}}praj6!SC~_(if)^jbsr?y}1`K1!j1Kxce)ksR zyFM{o;Kn!YB861JGBibP2ML_;2@*gArap&2 zMXQV};oA-&uZtKrSvC}pn?=iaoamX{94Ux^1|1TTzX+U6Z|&`ie0E)n@;(%PaKnD~ z9j7s!CnOn`FF11~u|1fYFSzv89-+yG&5Mf+nSZGF#Cc7d#^iLoCu>_!Lv|^Uz4)!m ztRu}u76262B-frFL7Xc9;Jjj_hb5$fjx55#^QWlV9sU?h%%wMgVU#Ncc5j?#Kd^t|4TniN7nc$?GXe{;V=t@l{U0I%S zc#jiK5ZhvN6b((}ZXFr7?wL+L8g+RKvP~ELB+ytQ0`ha)AQ4NLw z@ie554b;-;%-UlQud#i{%aP@Xj_7~V^fa`LU&cC&pFY#++HAK&gC!k=5i50PQM|!I zytbYP3CUccT_T&>LdP7Xjo191m%Q8l>I|es){N3J`EO#t8#2>Y#aPrsm~5q&kLxPV z8k?kZ#t4A3xqXUkY=EE0-)Zd++_G4wDZWj=aCPzv-Nt&uG-98&+a&UP^o$Z?)7#hF z8s9*Fo%7KtG|o&+8pCGaGo}1%^nf$_*Rkq+URSe?9v-i;ye@Izn=8X5M}3_oKBjD6 z8(4+5A~zwvi`p_;2-B*IX@KeNHR{XV`h)V&T33|$H2=V)vH$x$*2g)#jHG{HjpkQZr7hwC_oHEkTX1zEbs!W8xJ21lNolxE`19cM zj{3$#{UvYKYjiq*`pfJOqYf+C{T$Q9huLXEX@7s>rD&g5-3Ka$dQ$Hb66_*#n1P>l zW6|A{d$C)w#z2snDbD6^=9IUb%fG=M1H)H#pE?&?pLJ9-`^M9;e-_BTY?QB1+Vn4b zN2s1_J6x%tSf!Yn=OrC6u=Z_m@nN_)|BEKEXf~yi{54Cd;W55PVDKWSdUg{$F*GdHj6`prM`5GTrt6|cm37t+oskywMgT1tj#<$ zdN9tfp;THhNW766#J-*mAT2B7h7&4QpovQnmV>>Z%`&s|t{-vJkQ= zt#0(WP-ju3<$gyahh!i0FJ=)vbWM)HH8s9k4|N-^IAu#_4qdExMX9Iu?4>57GG0Cp ztoh8AdVSizg%ru`0<4CleCITQ)qCxn7YUb>HB|`*GgqF|xT>vmEdNMsFdHgVHo#5# zc}gqN`A|v{K;o13O4CuBhaT4UovH(hoNO%Bil>7tBVN4$gk*qGfPO2V0G2;tiyzD- zjo#$=E;Z19t;Vk6tyu#I*C`qjG3VvPldgg+f*s#O^h0}86P1tW?OV3p9s(W2BTh$s zlI?Nb19uvTVCOGW6$~7w1B$E!XnfWl7Ruz0%YQS5@JP(<5CaQXI$VWsk7dixaV%}_ z&}o-iVr=>GcFX<8hpD+Dn#k|>A4sR@5b$i;JoNGI^hN+6};8eIrerx3;Rdx@Kk;bRZp6aLnFZ)m)to1Q&>*Zk}OgXXHuiIaLPnQAOq z^4n7yDjcxC7w>rvMJn!&|FpQ0=&fpjM(iu#i@{_jEzP{li=0vKuM*h3Fi4B*Z z`}#vDS6B4y?)$fA0%a5&D;*9dMin!Aotzz_OWJ5>SN!TS61d2cxYY4*$!C!H=8&$l z?)>G^LLE0=7|bCc@Ofb-IwJ!NSN6iMIhbOfuS2qZvL(aFX@pQHu>jW`}~c7 zI%1`ob#KJ2kE;ekh&M4S%)@OJSV$+u-?+$UBcxrSkraiTC==adL0tb6N$#5ZV-lz2 zb)67Wy23{X4>+h(E=uT=P8YdFrOvz`byB1X#SGnrndV_dVkGJ%^@{|$(KNG6jf|J# z_$_>rEFlPvTnp~x9=>(xh~CS7!MY{Y$ZalqG95(Jz;s#5?pRu0qqxb`G#C)U;-g;RIO z!!8kKTtZ2ndh7Oq7&0>}eEvK)t!Z}U=wDdh{kO@i6^aILM^gXJSBdO<8b|0A$xO-1 zh=|b(J^FgI%h^mrt)}ciBK7Giw0`dAj}6TggJ&as6)OGFp}TpdEfaLNt0{I(On<@6u~_{B+fh)WJ=6)&(c3P1fbylMs_ds1l?2c|i))DH`rT zkc}lVQt6j?|P*F`a|}h&ffTp%~k6Rgt;p z4-xJUT)0~~)EPL)ywc`Ff0q#oC*Ii}`g|C7dgh6Du?q3Ak=W;6pSdSl_{d}7$tJB> z;%|B@HF42q^!k4P@?WbIzaV#vg|H#plg^y&+;Wh!gAO@FD&?(q55fBLxsSyK=iNVkZf#P@nTDHd?q@C;A z?(FaUOsT^Cz~TMsrsaNihQuhb-X821vk9O+}sfC5p@+cTKgsmQ%L{ zbq;1C?}eNRCBIk1?oGaiAFn|dk~_KEWcGY#ArsY{P}Qu*TKqlAmXBPxtXzD(d|;81pthL@a}y_ zxy??!r*|3dgbp-BI+yL zjnXcAd>HfT_CVKLW)sso-R;@AxX+g&im<`n9h+zY>t5%qol1FfV$~x(h<9=BhLf@Y z5VR#jMPk@UD9=>577X<)!`7sI1U1(}x}Vx#9xmK6b$jJd&%hA_@tZLIKlmS#Wt8GE zI7`bL`)*v`1gns1gSgBlY)6FAB`3~CWI0r(Xm^6;&P0MI%#LVQ)QE22zjsC`>WnmqBf#7szrF|#+CRfqw0EW>5t0A9l zKCxWpm_lRZmP~!}v>g5lx7aTUeX{)*cX-)aezd)fd)idDJ5kADRDHC20Gj0szdu|5 ziiBGlB|*c9kvM@?c0AClxrWdI1j+y7(6i^Bczb>1tqiY)R-ZH(QxMIO|6mC;xTT4| zyac$oYx=rsB?Q7w=c~IZE@Utx`sfr%5CB|dbeIutTh$dc!i)2vaXNnBMFr}lf!&$; z26q=-Ikhws`IyN?p=Qxg$?q|CCd=-&GKOCD?}Q(8CI_p%={GE<+Np~hvmM)9Gnbav z4!4TtTWU~a>hn>N+6tfOwaQZA($9vJtj^NG-wCnSlB1E@E(3__23$7LD4PbUsPlvW z!Np!L!zChC4>Xuz*__wwxB{6_G;b3w4}xoKN=~->zHJX?4w6h-Tf=Ktan5)UW(7_R zlPZ&@wNXLP07M`oW+l~%WRBHAgDxbAZIAmSiyQ6BGZo8^g%F;=mg$KWvG=(@f3u1* z<<)hNnTJlLTql)j_38(|B%-^>056glp@CLkGD(z6!i{;3$THx0;d)&V=bKu9^W}M4 z86u4UAaRp`_Tj*aF;s`o6FXiRZp$A%G0t>f>UDhqA>)<*WrWqVTt|niAtDrOIovoY zWPV@b9>hY)SINEhw9y~kde8sXIFZz1?CZ>|Kvoj5e|0Z5)z8V@Z%t%MlYt1paI|bAm?DSJjsI;cT4x%Z_g2juzjF zG{T=7XJOZ~MLMC`hC|UNCez=CE2Vhb*8NXU&K;As7&x0s1;cHb^>&kp)akm#0Dx`t z-z-47#)>#k)c^3!tpWU^4} zW2LTKd~v4yJj>*f`x0Pr|1XmIkwE)rZ|GLH!~eKc}-CS^Re z+slZnHlJW=n0~rCp#Bn0D2Yr}B$1{J1T7P}=pOU6)RLJ$FL(NR!X@(pnR#`Iu7Xd~ zblr~SgTI~Zk_I$z9Wa&NE#0HMK@;+9o(~%`vKl8s0Dh}07knCgx$D67aj)%Y z#{|sMMI*5;9rpHl4N(H0*O$C-Al@$%LvZiq%if{m?rs3w*C9YA`_{h=2Y?U=2-3>U z_|D0Q9cL1(Z)Ot2DIORG`KN*XYW&=iTUnt{LLI^{AaGTg42X% zR-7JlZh2f9b#6AgKrlBjl!DIiv9#*OH_Nl)sLq!tBzRCJq$_ft@+a!w0%C5`iCQpb zNVt1#jjM6<=x8JjSq=j_I4j^zC4Vxs)UH0~kLsVs^zi{(sV>T#^|d@-Yv}=Lm|D+P z)8APx766MW|4sm33K?#yq72AbZyk<#ny|3SP#&+%%ID7zK|3f&CCQAUd6eu>~<%^uMa3m@ot^jPx5&ota2%Qe|3USE5EU` zHItuO8ZaNe0z~ThU$siC!MW2=B7#P9sm;gGtgBPK!^77&C`st?jXAITIPS<*m*2GXm=*cX=|R_V!O zN?4**VC||()MS}&m|L9j=&U8#wFcIzO1-A0u;cE6H*C1u8o=CWY#&PrUPkk*8IS5) zEmACUMVn?GvbW}vI*m<=o6&)O;O+PvO5c~$5=a>@hSEE6VZ+s7OIjD%Wn(=um&5^% z6^UE`A)bEKl~WWeiD&tNd_%XLvu;#>lOC3*MO8 zdiW|!rC|!3AUliR{2q1v%nh;qw;72`2;hDt&~wpagTE0E4L(g_s8cZO<(`>Z`n-i_d_pKXMW%Qf;XC2>n?0z zd5fn)L4`b>Hizxf%N#oA^+}RITeZSNszECl^u;Dwz}ez!accV&_GEuwLGLLy0N{ao z&1i*WK{qV@Z^_nr84r{bUbU6@XD%rqH$~BR6o1*tP&2Hn7JKjV*|)~0aJK()lS>?$ zvDpM?Wr}NEC7;aUl$ZXM&i6tmp+i5xhqm#dl5AI@hd>o6Z`+2Mxun$?*~gHzQi1AUE&m!k0g-l)Z4NxCZG zz?+^*EUf?WT@2hRi*@D?HpcEp-433bDy)?l@y_(4aw|^^!lZILhB&9$6t0k@9g|0m zOD4beI*T*K;*+drl93oq&ReC*nGdF_F@p5*cKR*6UDDt*I9^3546QBkvBsA*>h>^P zi#TLoCjU@B^>O%`&F!F{sSp>r{w5XxQ2KlAW1X-hrP!6RVTvVEWxTc?_X|5x>s)ES zBu*B=?J-1ONcnSi68>Qb|1EQOS}spzP3{b^hMVi-euRlX(|L{GTVWL5KUH$Xad0h}^=_OQ>ch>!f`+{^Ox-+0{~M42FBpUc>p3bH zHMCTkfxE46-PZ#J$(z(W;W;9Age*VNte$~w*0O4?zc(z>*3#bu4`dPToj>K~o%LNX zsbbhnW~IbSIiAADBBuuPpgKu1@mJAn3KBv=SSRS<;;f0r#qy(~;#z=Xdw!XX4$auq z>J#RUv$$Bm)!x-ez|CWSw)telKxL`*SF*Wxh6sSLE;H|*Wul(v<7sNL6Q>k&(r#aC`IC8QoXFPhL_v z=3Y4W*dwPzzu8&vfJ=gK(a@Xi8sgu|2D`+L9~ILe43XS$p7%M3gDZi|Fk?S^)ooDD z(xiaM^-u|>!fv1x>%&}iZFBlaK&_WB@YBZj^1|)awsEnV)^Lug&#d2)@c0oj@VLLa zLp=RhUfhTXe3RSwA*IiJ$kxW|_?00XolnR(q@Gb5-oU`SxERCPwW|5x>N9mUleEV3 zDf?CcFb@#(X7gO*VRsYiqgvQg5z=PJ77A$o<;f&^Ck-MIp+#%ao=zYm(OzK4qIwfj zlKVtkSPu<)iz^4X@@jBEOS#Bn;lo=`^c$Rx#yGUQ7ItBUdWh7iNhx~njZj;o<>9gv z4r{0?wCL~Mat`Ht&(q9E*{r@e?s7G%Dt;2=O{GT?n-FdmmR{2u>xCzG?8L8%`FPqI zM*(Xn@k0~a1vh=+D)ModBW=2cSY}+fbE<1Yp8~+#xIh)F6)u$M?%XIrM$T-izID!{ z;Y!fslJ}p%H=kN1nNhUcBJBZNhUy@ybX?Q2Ul3k`!r2LyO@K3g zvM=YK6JI*VGApcIsOPb|p?n%hpJi8`&q}1r_zro;T4tM(w707!?Vu#1fNo*E^*p(ej)?*U1wnRzx9Kc{*LW3{IcJXm{reg_-E#mcYvl?*W_w8e55oY$snv-UJi_~LPc`)BT5?V&aGa@FiKjjR>}edvVwf2b$Qo5BdFtGIzx>eDY*!N5fhjt{BX~toCUnP7tr=|V@^7LV+Lbvh8 z5J$Ny^1sn1eO#j5cc*8BiUs)1-65l{d7oVLupYUmmH!}Jce1x216bcQ)J@L8*3u7y z$jvPaufh=Zz6~uICsGR-&20K!TC8SgVK0A8?$`Tm2eV$gnY+uO#Mx+hdkrEskh1o; zF+vmk9GNp;3cg?vu_9r?wZDig``CdKrf>;CA55ng2=uXf7iP(fqv&r4Be99;QYT>- z^;e25T9SJ%gq^?~P2_`niSDZDLr-G+%`$|+f5x%kb4sHt&8~&_BbcdyM$&N7X}Vm4 z9U~X$%LHpGAXG_5?@@DiG>Y6E5cYc!p|taqHJPV#_pNd_hY#Rs@^l{>N6~aXEmn%( zB1Q@DEKT@?(#J`)zw}?c2zfeLoK*r zP0pXrM&^Gh88-0-Zjz7m-SQ&Z>1n7t&o|>Ssbh?DQs(CucmVV(B887L(jlAs^F;YXre71>wEFA91&7>&wl1txuk7gV0-KE-8FC1*svfmGO zt#smU6K^+P7Hh36lsp+;G9H7~y}d=8Jv0w7+yafBGetnmE63{haOM@2|ccNd>h#U$E*uR-NnTH?fyN3o?53jEMxw}$tD=f>13zN)<6mv3?Ti^$E0*N8=Ba^l*fz-kRg)G z-Wv=?{Yj%xJ|p(wiks&9?*BK2tV&gFT(`~A38 zQ`0?DT~jsPy?U>`)(%xtkU~QyLIwZ;Xfo2`$^ZZ?^GBZ$5&GkvCM$OH(ZD!>WKtNq~@PnHSETok;tjnXH>;Bh+)$IqYSeXWU!te})d+27&?*u_(kt zF#p$qP=?gD^^ty7zbIT31%6lW6hDE*T3{l5%v?l(YJY8c;g1(KL5X;;h+L`fn!E)r)K)s2}9`S!TI3iMGT5c`DSBY0niD&Fs^O{KeLlR zqELttLv0J4`@&vxh~rmB{(d=W@1!f$+A6h^O!6UKNEzA$k1U#jMo2TfsEW%qZAe{+ zAQS)_9-~sUP5lH3OCK2bXJ{|)BdTfZWD^Ag97^i^2w zjxr`(A;JQlA~np?>+i_=P5ndHIMHQzoMmD{E74$JoR~9QvSusZ!4-DiFM$BckoPaE zT|YQ}oFtQD65nT6oHJFQ;<$8j!Vk-JH|uGk40y^r#&Qxxd@jEyibHN8T}QVaQv-D{ z0Q54%T9PO)4h4um-{q^z-xbIRGsB7~mhQ({wU?>cRl);8 zSORu&r3v$b`GOcDU1>;9r-j#=t;$!n99QSi%eFtUuc?4TP zjN9i`W-|!Pyz|`X`e~nvQ2U>P=l3Rbxvaa*Ge*%PiYOFOP%3OP5;4Lyv{{g^Q@0y@ z0Wmwts=S86J4(J>T0$jrP85~hccG^NnFTxO@Dv6_%Ue* z_^Ge+Y2fjqJ{KJPKC$>^QXaO@rw)o)d-FKrMagbKRJZ}clQd(t3;+bcJ}Tbu8?8s) zbwCCk0^_}2QTy@o-*B40#cB_6(22#PnR7;<4`WmOvdc!txU0X~^QLFla8=*{0yPQ?ea8%d@l& zIfDa+s3R38P=2K|`&|V=)GwN|m*IXZtAs?FOM%GP$NY)_*k}a&nyhg)P*N~}LXj0+ zTU5vG++nfNCR-iEPHqO2r#~^rJ z#w9PpT?|gcJxxzoA=wcr2AImcrBkhF^{=(x2;X)c&HiD6ikxwaI)$p}w9bk*Y6eoM zT|*hA3L!0TW|4otY7*@N`+*l%=QR88c3Q$*N56`X&)01^w=ifda9PKRzOv4U0J7Cm zvesRT{_T4HHqiYJx(NeI0SHvD)Qb_i<8d5rWgl&v5Z8X){;P;TuBd$GA?X^zLpF7A z6_{4jkww79x%9Y0=rQeF}0G99Iv${t|PaM9!7Z|O5TxU;rIS#nk zBe(sLo?VZxm9_8d8kaD?e-uEQJNw!7tOb-X*`h{V=aQhS(X9R}d0kMtDYOk{DSCH3 zsD-q4Fvl+^uFH!}--j<1J3Anu3pF08%4$TDv-Z~#{_Olz%$vzr|MqAOOg6h;)A9PN z``0wsgSc&D?qP*O7n3g=Vtby@E572ol90~LUDq46gG|cFwcw8miH;kd*1ZT zPQa=aW6@4W!`z{v%b6NMRSP|>-AyS;dXnOkjOdl=Zqa+H&`V84A*g6DZ2X_Fg|;9a zWk5`h73&sFkJT3ckHnyUII}0BGj(x%a$~~Z?{LoHKH97BHu!8J_msQR9cX($OMXR` zSW-a(((X6SVE}*N51o6JgO2V+l-b54myuPwFm4FqB2c2PUz3KcXWKCp84>C0DfM&! z*>F+IP;!k>rKp@X=*{bMUqA5m3*QkZwiS2o+&A2)#)%Y(3AO;0@E-OCEDL2@;zn(2 z&x)fnzmJTtEl|R)bZz6ZLeUgssuV=?88&ps*j6>y9L8Xf@|Ih*3nj`|fb>Xbwmo=u zcM`W-9sQNF-tv|;bH0w8N?Hd|)_2^{UkT4S^6$dtJ)dl&saw_(TI8e%!FLkMpBikF z=s-*RC2i`PTeYzgli^0DCB|*RE(jbU48S_{q`|IJB5_>`Bi{6;_G7_m@9aaJ@?_+* z*j>EaG@c}pTvvzic;vWurbfq+c|ZkKk(5-#KUb+}NBx#=U|aWDtbua( zAq!d}M@-??AzC4~reLBK;fD!&GG+%oXmka%C>7>i%1xs8%fkaZO2|-oj31I`a=FA1 zQ-zc=F8BNl$VT55&aS{}^0(?5G6rMuIQFKd2N)0z!$oaKV@rFlc|tJeqYKpwMyLHv zFM{a{hbnVCaf|6dYw6IsNhYZAz4DMOt3uKJ^)sPNC#QS=-AZx9`@ZuoN#jZot#kJE zeD78p4{@D~$B!@HrPBD|!bW~lD(@Q;tqT%r<^<1!HZ57ArIt@~RLQ9TF>DCR%xRrL z`sfiTdRoGW%6qXLxfMJ3^zMK-t`RTq(nOevP&MfvQF0IN-E6iu(z6(4#&*O^rc?D; z^*A5%6Bsf=oY#qaHu?8PSokM&OvjHcZPgy?6-4ZBG9sc><@PK+z(;Nr<)kcK91lV< zX$VnYB%AvgQ{u^3Oo!0;^=y{!jX3A;p)lTwisGupGLUx4+Pmfax3~OF-w@M@@yTp= z+p}v9#f_&1(I8Kb~K$CdwgDe`X+v@++Dw)h_Q%h4*-) zE^k*`iD(FiLKy$pcVg?EF21`N1`AadeI8|i@WHn8e7gk%iVr^kah4+y0AHAQ2ysa- zhJu`6dCA7wMER#0@#E;sq@aA5&?86y9(Y5JA%tkzh1s1J^b#{B1bZT}bT|6ZXnnLO zE)#mv3dc#769wFR;x945@j18MpABH(TUY~!4+@GGY-_9`({(X5v$0g~xqQrsY$`|O zhKp1GbmAi0F;RRh)f}cs0h2Am>Rb)9kuxnR`n_Aw#0Bpx6vqwX;BVcCKlVP*n2nMA z?i%K3B|0f#IAVLeC#`P)5ZUvL8NB7k7XH+V)Kd%>Pzo4#Tb%GW+&mTUD?Z?*bBv&) z_Hfc+V6*O5{Cf`|MM@r^Fu^67?>6?E@Y|F3gDw?IVSgDtoJ_-t{TBH>4BrILjnHWs z!98n5+pBhtok!)Dq22hq5#;k=*>++rzG*ZuZsZjv1%`{)n0l!;+XoM*P0<{10KksvWT;!Oajg zX)E2VQ1ky_z{{_3C)%Vy# zY`v=$Bb!7_5?ME`$n*J`4l?e4AQG9p&VL41V!ZT!#D-(Obbe;|8X2r>8&y60$A1{; zcDQ0$R1g{t{#Bz9*dT^pA}N%5!x$A3^WzIo-^JW?=6GcMHDoBC0K}&=QXlCeJ zdQ{$`btcN%r(RWG))5wPb6!OV{zgEH1~i9}efe9R01u)oV0VlfG4`yiZs$ciMc6z& z5ROgIuQTgKlkNYGPWw4+vpNIy!2-(69?(4l-vU)Y97grP6w-#iN7B>vTM*`$kiZ1d zZ8{9KTnkk}WaLu|tHjk$$$kuQFQSi`hKw-cx`<3;X{^z?+RV}mcP-z5FrOQ(4)#?> zCBG%8UL1T5(Tn6B!9`0BD4((CD>jnjUc(`eDsLl`qnLm-@>6j?lt^5Y4B4sMT-&tg zZp)re{pIhzAQ{!bSs}Hpwa7Ttj2CI4|242E(o84(&eLoqaG>FUk93P+8QqwoAT6us z9{!F53wNuS;u*Lj9C{Lr#q-5hZ7Y(_1^I}$U;bwFYhkPAYrYjvy+q7lFmJ&)XrN>0 z9j%sB+%bAhK|1DH&P?hqJZkTa!qG)EC!nlugFP@rU-yc>uPXyRmk=~`b12iI)pDcw z%!rY~=l1$)h;2X+=stNQ`%{FXR{RFVDNq;g867|w{Vjo|iS7_E-5Cv@Soo1C&_fMm#_ioByr(#I zDquS0iLRHB?)b#KBKjvsI;aBsXjL?ZJ^JM5n5+=zQhE&T=d0)P6gn~QU1oJ)o$H~f z%=qq1ts_p%E8We^)D5_+z@iH#4?T%Px|6UaS~Qj|Bp1oq%%_M1Wr;8h zGKTUb{jRp7?Ha+cEQXE8QIq@p9ybx>4+Ca+_#Wy+?b<6n@zwi(46z;0yDU0JoHI(e zv&G2UaBw7Y@ZHp#+DB^8h)bZgTZRLWR3+GcGAeecwVRF-CYFN62R05S=^n|uNj|@xJ#HhXElgx-X)PW=qXMDE~rT<%|a#clToR zkls1Ga{A~>=f%1R>d42&|9g^PZj$KkX=`GU&%_0p@mKSkz`3##Ra5dFV*&l@mdZ=SPxOHmzNqlS`2~8Y@F^)sBFm ziCku6x>xznN|(?dY5P)87{1bFFXl1k&GrVK=0rD}gO74naKYaL-VleXo_3gl#}Jb3 zrp*dd%%23j91J*7DE^~+$Z@u=0%nY;b+NCA)8~GC?vsi@+Yxci5O@eG(1=f%5vp7G zspJGIa%Ax3+L6S^H5Go7b3`m;?V)eh@9vjwc9jasOOGxtVLf$d&3Q&dMonp)GSc@e zoC=_3qDjkjW|SZA?TOLYIJEAYtq_f@Mt;nLV}C{zOH^U?Ur;e%^O7}HcakMrA+A6pSr*eReHUkRq?ApvK@hK+vw#&BFhy$9G1>!H2`bFLo z9UO=^HcvOd0T4M=1Tum?vN%;6+c%g*t`iB6xgNv^z$x{97C9Ip)x>$VeW z&CTu3nTTJ<3fj+D|0ztAN8hm#H$CmeafJ`Y!q!l+*6;!~dqOpaEp^=-{q6!&1LZn- zT}CV;W1nX%4R@vsA7$9XG#?J%niS{Df3RDZf1xqR7Z#YQ=4G{W3L@>6Zy7+4G9963 z*y_*~aAetzWcFTEdD}RMfSh92YuszdD9*}JraO931{|0MI6fEYeC&3poNc9Q1VQOJ zX;&xCQCbhJPZD3-GtJihO1pBccp?_WG96sA8HsU`a!!GFr9l?Ji5$d! zk);^AZnqD8#0!t8(`OL1R?7FBH{iM|-wMM(GXJ!VE!^_6Sy|3%^>deq2Yx3R8#PTS zr*C-(j7S!{lDo?D{Dg5Nx58omI1yra`}rH#O}$}%DYFw!Ew01-Ca>pP)?UemS7lV} zyIm^;vE>$(qmbnwz}Z?Sx|6Ix`ZhZ4R&4zP|peKkjfTT@5=8nnpN9sGjZ;7L{JD-&H zfd)zt(`Es5ng=HCk+8&q3>E8LRbH~;$VJWeMmjLy+e}`fNpUg1zl1{U?^JORy!fk8 z*(Z@8b?OXBSHzlyb~~5UoDir9jJvJ%@8*#CdR@#ICX(U=C|egX?IYvUJAJ#fCBmoo zwO^@t6=@0OIB`BC)P=gK{71h3Xj3l6?KhcmhxULTc(T&@qWq!^-A^LY2pa6+Qg%c1^_qHRj62{BUp$!!3ImR>yaVJyeG$a>I~f}}$TB3UlJR2QJ3_7&tMaXGj)oG0 z=J^ShuClr8;WWmj^*Hm%beo`)MJFy9QA9S5F?`xRd5C$gLPcep-Y+Gw@>fjaPo5yg#U}Hj3qt+a|%y|%P7yq1i{Sip(J$a12Gq`U2UVWw|9$0&VACVfn!B&xmvrqrD zYLM6B`?b(2jV-EW;sIaKV%+Gnj{a8}tX4gRX1puZu$*y3>d)mE_AqhDImRdg7376_ z&u#Hr#kt3@zl1_3;AK>I+N4%47lS8xF9dx?Pp+5+8Qo3Gw=N&!D4WNb~GX#UHn5E|8xUBj?IuIuQaOaqN4ot-ewfzuWB* zEMuRTy%=sda%BCz;6Ivii5KwXgvc0Oe^ihcJm=#k9deSgYM1|i;UK`yOHcAZHpUCz z-aD}U7o|1y(Ay|KFtEC$LTOUnk=R#Xy<3ne)Q<^oT!_@el#7a}3)gkbmIr8p3!sY^ zVF;LiMWNk-irbR(|RdO2olJ>0MXETXpGj3eRkq zya*KLNCQq;vTgrU`ls4I?nR(`qxv$AK+IU;@@oY{}Tb-@b*R9qpes?8V(Xlm0%sVLM4ql(!+S z@+zk~{y>NbMGw8Vd?I`DZ%b zjwjFj&rhnIf2=bhI6^|Jv#D3hd)`^4qmgLpUlr4>s>T_gj&x?EOh2~@3hwj0S}2vx z%*>ho6<9GDjf%vcK5J5#?SHj09I=2z{AlPKtnN6&%yNe!AgyZovhYy+n&_SN-Q_K} zg$$yT7owU~reN|CJquYGt~Oto>I2UCkc-54;>B81QsLR*xJO{JoFG!MLis!?vWn{4 zukmUsJg&hvGB}dn?kha-^?ZRc)jdTh(S?&m2ZV<`T(=Ef3Ov8KwQtt^SJ&shL3IK10b~fyJ_y zPcDU84^RD?J9d*s`JTcz9I>yMan=Lfnu0*A(v~Cs%ev^?3!U1;%xXF+ZTlQlJNLa- zxBF{IYxc^Sr#&kb@zzhlGF=+MTwnQGC_TxB$(i46`FL++mg*|SXg90Zap1=*;cv-r zbp)M5tQ1DUfy|mTUt!Fp(j-WMkCG?ey=a3HVtTeW zNg`|BkumQxFGabjKBLr;y{0S_^oWt>D8iRS7o+nnjjNuK^)jil?CkFk)42{GoWiTT z#p0*h%t`dUkY>ix$px|b9*5q@idE`z37PEZbfQQidJFL6jYkb0aA1l!(u5lVyzv%q zy{X{ceS4;XjBM*S2ZP_O;-Amc7gslQ99X7kesE+EPW47`L6P&TW(cv31W18SW%B)@ zaW$sZ{64VKw~DW8@u1QyE=V?7Lp*qk6usW^b}(rA*$qP03q_28PVgWZ`wr=hYRHjxE0C^Jnr$jaGu7xydAOm$%^*H#DM#Z_|i{j7{B zVu5Mo3%a{|dIHt?=R~|qN#n3`DU~erWLI-RnCwr);XB%sjVMuCK4LSHLk3tp^l@0w z2gN>NpzZzSfqWU+r$aho6c9Xs7{>jR!QkCR8A&~uu4|%MbOhQaG>Qc67XmR??(9FI z4vAre*e}>W5v9;l-Z)app96V~+`N^x*ah*){?v=*L2-%Tpe=y1L_;lM0%9YrB^n!w z-O6dm?t~6!o>I;D0j>hGUz;zuKbUq2I%lGv4t|TREvs_+oR}f}+nN)$!G#>;A9_8hBviam{)X7EDfEOTsQt zE*GDVMDcR0bQJk8cqP|mC8>{!*R}4~GDSJrh<}FnP8CN8D&HzN!EOq^&9So4TR^Z1 zo>*p$Dwg%1z+Q|pV1}XhZ$v!G@ZxU^qS>r;D`AHrp@|ee0WiWSfm@+VI)>!XofLG8Bt|<$ZD2n^3`3@zXl@tOl(1uZwP&#{C2q`T#7O z)UdLP+fp%)s^@N%4gw`MNmYvxM2$WU&c~ixvwGP`UApsk3N2h28>@Wup7zYp8UwMG z+7~?MtBbPfu@eKwDGzx4cG9OBO>I-V1!|pR86Po?&f{x?tho02oF{k}4x0^Mr;;(U zz3o9~eTMAkuTPpv;Gak`Mgy?*Z@mu(>-cT20IhlRdQ0Cjtbs@1YG4BP)1T_BK=X>6 zFei{kG4Qqvv(+dbR7S5;%9X*9z-$fGy}IK(8Apr#q)hOiAjrS;a>sbbkn4o#b2&c4 z%H1g)G@Vw)cS^RVYJakxiX8`-)N}B3DatZwQ7e9l2YVP;ZcpH?aHf^4y%D6T}k4;1! z?)xdIGQP1p^jGuB$BI4=Z`pk)d3v3JVE_Qq&;M!xY%IpG&bgSmtTo$^6RehOSF3vVaZSqma^n!qBwOS(XQ~B^yADG!ZL?c}~V1TmsJgE_+rbd#;{}eCj5hk z7rtK5EJ-XLVSe9xqDf?D&R`c`UsfWPcr(WJ4dQ&(Wf}T#DdYl+PG5Y7zN)04xjPMp zGYU^Lo0Zo1R43_vE;|^-^W9zLPv5Y3u^(TWv}f}8w~O;V;KcC|9@kn&vY3p&0oHct zgR(@P82!&re0@R8oX@0!V}Bkv(?=ZJ-Q^Zr>BL4y*~dgj>`Rn^p^~b25e`$Xy)~_;JGM~F&|iv&1ZuF5o@0!fZZ!3w>BO5^j6ECE4`{M30rtwU~63Ji?M1=N_ ztzQwPy{L{~!Hij~msoSOry1^5C^}$;q1!dK8F5-u+*nkb>VPX>ES>#@Xb1B@U09gj zLQ`d3(;(vO#mwPCVLw=?IpK7BW@LlV0=k4x9KV5L*_0nMAsn?EhfxJF_|AonCG5S`tTh z+2hY;OI)eH!*Ncz^~a(fq@`pB(3q>!P>TUI#$w z(m;e>v1{}^v(TzqJ;zf~aa+powil1VcjS9`$+rR~IG}GEA-K$+gQ-t2nGYd=Q(Rg7 z83>S0K``fRCox-wwOHD9t3=$GbcJoTt4_GP4!f-Xa6(<&{H3^giEhjE+er!Ud)7jp zqG8gq^=Y)6eGNC*d)6Wni~)LQb^oFgfL=s==t%6XPkGJIO;){w3mBMhhg?_tMJV3c ze6+>~=^!H-J6MD=ou?~UC1UN_tq%GPRnvhq5uCV+ADTZyc$00Gme!wqrW@csWV9Sj z?)-T9I%x>89Ht(g+QnyJtp*L|sVvL*ZreSttGZ&+-#+@lsUBP&4#RJt>1%}@!&^}K zBEgo@igmT_foXR;4#1JRIsl{VCeWN@tC;-IL`n9cb?UAtEwI?sB!{bCg z{9zmOtHP;R@%!6+Ol1%TpIdk-!yDeJsFx93{=89_>l#m9=W3=B)P)c<`N0K8pg!z6 z(Yn=-J@0;&aa2TDH?#8dVr1)btz%O><`Y)zvI}Btu%xROW_KQOzN{>gqAT>|iLK(h z*#rs)N;HHYr#tBSABWKLOy-c)^>jLGy(9jR0k5@1!;c{$uor4>AZW3@irzqLfz@EC z8NKqUXCnWi*7*zLFXul+;>ZDaQO4}3oMw*~)wKs$F89hcURIsGp0 znNYyvfO_?mfa;Prcxv>&W*pN(7RSFH3HN6ueR73M%FN^y^0@_bJ| zeIc%+g-SKknRY`GNdMswV9$k3tB-CLRdx}VAN}*fy6mfcj8?m|9{b|eU}*rc`8Y;Q zz1#0c$ncf@9?YvXde_1t+{tHhuj$4hqM2wiH{`l6PaQz2pUNxjSo;QHVu?1zGKZ#p zhkqK@Sck9?OTSHgf#o-_{S@9cQP!(JxOM5_geZkci~HDA{Ql=W85yZCMm$7rNr1EO zHKoDEZ#!)56WN>)$yEs_Jd(raZ;@wTvKB1+NHOfd8~)9PkK&Yw?IW|gZ@~o;fY8?H zV9i4UfK(12p?AkV11bOC34D9LuNpvp1B|?9X3bprmT7Y>I-R5N9%xuzDO_htS9QYGxhzunqqSD zrw*qOstn=?Cpg-}HuC{_js=%NJ6JoNADCw)RVZi{;)hR{{!Te=pRo5pq!{~Z2EirD zAI|aJEBd5;r^nW5}{&A&hn(4NJf=#wK7-C zHwm3b%QwSBL3D!I@i#^Xoz;q0_Qk5%x2~hZt7l*F6KE`oAi(^Gkx*3xk5XkowM9Y4 z**v>~5uOu<`2STa_J4v?{%_d-U%E&k?lD2G#H)J(_y4@hjv30IHDLX}^^N}%o4ZkVeD>DrmoK5DsnR@BDnaajMlB9pS|6dyvG?9Q#YMz~A{Xytl^f-0hP!}I%-?@S%MY1qaHX@($e?Oxm&P-$7 zlwOj3Y+9!<%TlU-U#s%);BrDlD(cg$NS&Yp#)*EqjV+t#^{yyC*OLP+pmg)Ic52i4 zx@L!BdT-=phD||LSvZWwAm5uR?EWTW#XUj&;+MYpe0$g%=!+4YKO&A>1bSU8p=;;o zk`65#m%=&w2l`kV+`jwUZ6z^<@c5*f7eBM(WonZ_vwXg^D)$>J+r|3x&fe;z@V~hm zmru`0-~cd>d^ zbXRa!L=tf&O}?uy{JEM-Sp@)M+?(yHUXEV3_O@$j?>nltT7I#DFIj12#UoMyS2sg% z2^>}2D3>mq&9?1XTt$xQfyY|2ZPtHM)l61?$d#QLHui7Mbq9t7z7CTlNLs$WKi?LU zJEBDzJnf{c!htA6sGXFa8yfE!?)}+T6eC~3dN(TG_3XFVPzRTq`>?N*;ym;46E8aVq zR?Hu-#N{&4q#QHEvLKF0LWAT9FrtWS2k2_aI@Aoj8w-aYIU-K3UgKgN{jTpQk0kIN zK;i|9r8K2Ol3Vpl_ix`_VQDbi|0JNWj0pii_z-J+yk1VC)4s=5_hOu`#6q0ylIa}) zKpC{sX_YnTHf>P!jh*0_Q$|t|)Qm?gHwjI>X5lEUiyAT3XHtNqm+KihCZ@4qKO6eK zhYB{kr1&V=Cevj-t%p(qK&*H&!#ohQmP}8W;Z&2qGVCkbWp!7q7;r0}0pd2$;54I} zD;yAgyt2Z_+CpIOhodU=us)TtC1c~cN%?mYXTWTGOzJYv6nb}?+A|F|my*iL;t{CH z-MMb(;qR;x&0?HeEA|11uy{m<4d}vP1RK`-TaXO|Z;vzGS_m?MEm2sPHP9M&7Zcd= zZyi1MZsQT&ZubaC1OHxb7_9wE7s!weB0r3cSI#tQW%}vKpAaP4#nxI`a?l*FY|x|c z>13DMi44=j?1?9>$o^1N1Rnb!ZjjCf2ioYOd@~Tc+u;;L+Vwd%%<^w9|LbT>Qf+hg zIvIy*5LZQ2VZzE`Gp~JrfiAaq5g2$~v~ysMeZ4KLKWmv=Azop7Mv|U9W`*<6hbG$l zx@p{Vx9sU@mv#Ts5aaL8ol}aCp)Q4pk*6JDELp`@wXZ=JD%v-j|IR~X8U0O1;80dN z0KA1yZ%><|gvJ*Efof%7UXMEhwuA^tgy6TwC(lytzV2rkC*#zmoK7KHzO8}0DSSHF zOYUOTQMS`Ho!R;O*4p?hI$E@g<=_!LH1A(c6mYQt0WZ%`Z;!@3A_Bk20-eFMr?*Z{ zaY)xMyMwTp3Fvs3L1IO`tSV!j>bArm7piltkfR=|C#Pbf_0W$}n4Cq8P~y<=daf0V zvKo(98w>Q--B8OurC+dx42B_XzPX=O?rtZ6VTS2sfuJiyHGQ@lZNZg!^`(7Qt8OUw zsx>=@;d~Y>YT(zCU#}d&c1l^+2HzKzBI~db`peCDWSqCN%WmyFn`|)6%jKPkXsg9p zb7Cb}^zC>D>!AG`>$t6Tv+Qx#eoIAlVJ>5=c z?{7}>!X(l32w&{3fS)+|! z!uSEcL{n4u)%T|Zf}N9`+M6aHe|2!s0t$m<|H*U>Ueumr;M+}J4`1}KNkDHPDsJ?) ziYy;<>6PHiVo5@T7ZAh*`i&>&(Dkre9>gAqcJscZJs=aLRm=5mJ@#PCOj=eE<*_l zyx}9EjZq--Q4{v^Pnm@)rNcEnk9GO{>kY??%1IcBOq#~oJ@dkRLJTQ6?S`k4G`=0} zI^Aq3(o~6~F>IoqEo#CG^XpcBhHR^~YBVy8Xe0IJUor*fNlts2g?5dtYA@+%C#yqC z2ur2HA5d?Q?Fi{CuDPCu^r~4oC}QvGK2oa)6ijEG$~O!`;4`~iVL~$i!1>Fw%l))>+DnrRa52 z2p^?s8ndF5%^zL{=5+oM<)lpDQ*dPT;c zL+s-Ub8UIXEME?5Y-9TEx1t#EW%| z?h*2fmUL+2DQzo@{K40rRqN*Bw2BI93hWEnOC8{;Sh>~29Q@;xxIPn~@L^W$d6XC> z0N4;{E2*~&5-WT3?ZCWTx(++HD?#E+1#G`!rmzqcjH{jOle6P0D3l z_$X-bhGr7b8!zX@p`RJxV#Os)0=}mYySy^bVuMixMuQyOmcFmqj&{Njy&aI|?p+_H0`FLG1P6-PD zR$+R=p+`iePu{Hck8Myk#2zAk=KC zsV0d4kaNzk@zOw!eq_2giAghVepg;@VWWB3(~d}nY^4hiW2=#aYI%J~mpa9mrJD1+ zW6+f2f>;0oJBk~;^~xa$HT=Q;-_F5i--wJUwrX3i=lfy$lM$7E(AZ4k1d)11YYYFq z=731mh(~EW0+P*?ncq= zGHw?Wi=p1-TV|XgPlJLTKPN2fFa6`8)U2?XtE%tRT^nEpExmD!@ypr1!dP9QTmJ#8 zG4lSIr7`&kbbJUb*@1zYSsYP}A`Em2khMPY5b5f*87psY7i!+$D zcF#bhfNx=aKhegEDPKMhYwn2RoKegr|Z=c!RJ;jo4#eVBZ* z+o7zl;@a6tZB}dVKh!bdNPmvYeXU&a{7BMfVDK~+c@rplYaf&r9y^T(1t54CDE@e# zZjIX4R7uy^Ps_H(CdRevIp>HTAbU-cr|o|D6?_0G<@bRz1gPv0@)0LZx+g+@nCZqEvp#?=A9 zyl^b|_?Z{&)aHcD&DFBy4((&?QD9}aQ`A5jsaj#)guxG(OO zqW<&Z;N!|Q-t3c)B-rT~hXQI8-F=wf8+Q`KHZTj)@poCSoSKiee_Tw5-^R~C0q;Tt zk5ReWk;nq@7@mN86^=y20CUL_AkF<(#ZoO#hr_Oc9f(YL|8Ub-(2z;p{`cv+RBf+6{agG zqfCA6hpA7QXu}y7e}5U(yg5iV;r!(4m$5;-*m{6mt%VjpFhf5`(PLeM4nvJSzQi0> z3tv@P!Zr^k0ZC&x>13!maIZQAs_;a=?d&De@fPblO*&K;;INHc%YLU2C#VOxggo_Kl-*FE>qk6-bokS zwd~MM1q4Z4kQO9Hkaaa!8PyaG@h4iST&OcqS;R_`P)I^R1Snl);mlKiY1 z+OS_#jQLp|FGhP5&{-x`(I4O;6m`N1LB1;v=%JK_(Mg2Bm-XWnLCMXgV9zT1dn&jg z?sJL&Gwo8J|8dDCR$(7Wa*=@UwZ|(nQwL|I5r2m8^XA*Q7!_z>Sn5yJ(48;1#E8Cp z_V|t-!9oVCCikIPdVH_0Wslm=?^b{?mKLK_m^xNjRD?m;x!dfkJtyVN$@1iQ*{62uV*ww=EC_i+m~*aW zQGJ!CPI&etSh61{+t%Bv3bJ#5-q=%t>vp|R{W-ja6$ZS7H8zfGfNAC zWYK0E3qKXGF);v>9jx6(ucJs7IMDZ?k!|NTCK^qDDm44n`~*3Mgo3>6baU0ep=T#l zF#j|NC(G#eTfZh?;w<(NVai)VXA|Zqlc#2Pczb#o;BQ~t+?K8}RM)h3I$oWRyW~%4 z7JiZ6Yvgs+uPksr+V}Kr4SV!*zYHsc_MbDS^nsJH|GAs(AWMT$R{F`v9$dX_lb6dE zFJ2@UV&nKTzp^I3aw<|j;H2IEf{dTrD)%)~u3nLJ>xd%JjgM2>Oq{Mqm}rqp7Hvy@ z)hFX2bKOtK!m`SW5mRh)cKXNft9>i}uY)&Sf1lL&-{y7_*v=eU>VLwl3b+Z}=Jm?@ zIjc0bBkBrRyLG6jWUEA>=(aO|b69EUKMt!Zd2sU)?%D_zo5cs7aN&f5Ft7bzZHhw}_eG|FUW;A513 zRD|T94D53XZ;QY8eqt^B%hqBu8HpgTb))E&Qy4oZ8VIzIp0-eBd;afVlAXB!`s#~J z5vYyeib5F1eBY*7pjx_$>oXh?SN3f9tQg6^BJujW^>l1X3?RQAeT57sil0@vujhQE zCbjo&jTVxx`ik%95lO?>yJ`2bTs=*8=k5vnVYt*x{F3(&XCW}uyT}{}Y8YO;1jq!I zebA9C`IeZ7aHZzOAPLY-z<+v+4>uYLnD@LSM^LkC(u&Uxbtf@R&XxMVe#Or06{z{C z&Q&d}dkG2nY!;GVlt-aG<%XM7qnjjx0OYGSQS_K?AT037G)$8-YU)56iBggqvu(fG z`gTG=S60C1W@wZ!azS=Vg@LY9rRb.Hwr|tlrd|)1 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 literal 0 HcmV?d00001 diff --git a/screenshots/time.png b/screenshots/time.png new file mode 100644 index 0000000000000000000000000000000000000000..39d5210fde31641aef160d774e958f9220bd2ab3 GIT binary patch literal 2456 zcmV;J31{|+P)X0ssI2x3c@Y00003b3#c}2nYz< z;ZNWI000nlMObuGZ)S9NVRB^vQ)qQ`bY*g5g3t*700}fnL_t(&-tAgzP*eF8|KHr3 z7kQBcNWvq8R}vlpqESRaR1k`&v)a}5(OO48wB4@L&TgGf+v&C+cKW4LcRRbS)9JKR zcW~Fb)|L8L7I#HKdC8lA;gtj-2_bKikZ_aS+YeC;5RwAQ_7XELaKLmkAJYHN!FCqN?fYxY?WIqCdP$?^P zdza(Ndf9+sSo_E*jY0+hgpZ#P0ObWz9)}%Q*2@MQfX*?M(Q1nnKQ~BnGkD1nsBl>H zO1VUomP{ohR)WMXmksWw>qpL+l2h2!?k5XKF92EHMRB)O+5Za z@|-zRYje69Z+5KAK~iGu#w(NM1#{WhJ@6rAz@A;GnHfh5G#cDy`0B%Mr#}@zSZ3z1!vEGxMw6OQO-4?mQf` zIo+SNbfbQ3Q(;b2QhTf9I-|9zwQm{qyS>X7Z{F`#s$l{&Z+7sxObCKt0wfU$#6ms* zAc@T|TI?^(p~s6_9IoIcN=x>5(BX;MsHA3%re4)#m@;$a7muA-ULcK3rLJ5?rI5aS zFs#v;p88j|kjKR^?CPC|R)=egEO-3_q$m(|3_bbpH|jP+5GcvZ2o6owMJkz?C*%ho z9Uh0NGg@P`-|M?KxAu(L7TuwfJPuQDdXn3599OAzS?MWJNo|md+6N|9h5-z}-8Hg| z`px!52!gmQnkX%qgus%lQ~*E{o4H_Fd|?hlQ7IR94o^j`8;mx4=a?$|G2vO-%MuX? zQI7KkIcWfZ_JQ&BOQ4*jgeMXQp0m=}AlB1edIf?xwo~ zZyv5`s4RWwM8mzl@fFVqqM=g$ySI<>IIQ)Y@VLyoeQTxiEc@6jT8JeuW&$~~P$VME zpi$=L!}v`A!_9UlkDDkJ33^5~Qjq{AKx87~T=GUW-t#zIK{}5u>>8ZCxl zQSA3XIp*?sA`{{;Y0f3@qRRsSAi^-2gusMn*+p-%006iw`noVe_M*NFKKDTN#y?;A zT4%CRpVmGA0KLiPSw@R;(-Y|l>*JEo??Zil498Y$964OO<>IZ5^-}dH)x~0=R3rcZ z6z64BY>*%@94+}#005}ZA9mGXw)2x%Vv(S^W0=FFWu>KLN1<hnYeOj!2`{ro zXO>Ha={(Ntyyb(&<~6I5Og;cWy*dB_0Q4sN;kvSpp~;7%Y5)K?k-qS>eyiSG*fl(* zU$6lHh9(}j^{T2iNJMhbCu1 zY@k!ARi&~!{ewP#AX5L+=5)0WkL)Nd4x0-HR9d-Ic&vWw?=Rf+uZ|448NBk6Jcn~h zGq=`$gDP#NfZNpC9~_oSGDoN9p2QghfgtFaDlv>iLZWt$etrIGXpP_x1g_ob3yoBM z4D)-Ii7=cm5?r_u{b)5j@p$K^{2rwm$8i7v1VPDcM$pL&W;+pvRT={TU|c)@+D)C+X~E{`P^yBGtU=!+O}|Q;Dd3(x*^rZw z0uvwr0F^?jE-QTNNG$|G7jE45qP{hEJs6sFxR#FAm-9HRhDy0mz+DNs-OA|-xui^% z!)DMF1=%|l`CY@)q4$1kYw>$;9ZF_1LMK=1Z;QjVjQW>R-%8(rNl!SqOTp)|(s}Gt z2dWGfhf1T5s+t@32kBJG_x4qAS@c8(?f9PZA0Da>N@uhXP+~f`oK{rvQPjo$Q!Pg<6q`rX4(H3AcA%Vo#v%guJz zKU%swhbC8_{UIU(Lxfo0bKy9~q*LEMQI7@i!HJpAW9yPEx;*C^n-A{V_>0r~0RSUY zv*#MW2$uC;)HkNq1q)6L!&F*>*}e$=GZf~flZZ&{!E;sBnw-2*;_p>WJnd9J|NVPm zRwj)?$xh|3Uy|jfZz{?b^0-V!g3f50F_><5k2;)7@&4rh2BVr8x7VxI&qI;FpGab{ zY80h$eZ7P*I~-s24ichnifiiSL=uYyp7;JQj#mY7`@PzL;@=Ox(m=>GdRz;yF8&SL W#66KzXm6kZ0000VL_t(&-tAg@P*YbPK6#P= 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 literal 0 HcmV?d00001 From 8bde4845e0c5b56aa1086ea6c09aac8b1f73d08c Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 13:05:19 +0100 Subject: [PATCH 102/104] [theme] Add ASCII "icons" --- themes/icons/ascii.json | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 themes/icons/ascii.json diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json new file mode 100644 index 0000000..613f133 --- /dev/null +++ b/themes/icons/ascii.json @@ -0,0 +1,56 @@ +{ + "defaults": { + "padding": " " + }, + "memory": { "prefix": "ram" }, + "cpu": { "prefix": "cpu" }, + "disk": { "prefix": "hdd" }, + "dnf": { "prefix": "dnf" }, + "brightness": { "prefix": "o" }, + "cmus": { + "playing": { "prefix": ">" }, + "paused": { "prefix": "||" }, + "stopped": { "prefix": "[]" }, + "prev": { "prefix": "|<" }, + "next": { "prefix": ">|" }, + "shuffle-on": { "prefix": "S" }, + "shuffle-off": { "prefix": "[s]" }, + "repeat-on": { "prefix": "R" }, + "repeat-off": { "prefix": "[r]" } + }, + "pasink": { + "muted": { "prefix": "audio(mute)" }, + "unmuted": { "prefix": "audio" } + }, + "pasource": { + "muted": { "prefix": "mic(mute)" }, + "unmuted": { "prefix": "mic" } + }, + "nic": { + "wireless-up": { "prefix": "wifi" }, + "wireless-down": { "prefix": "wifi" }, + "wired-up": { "prefix": "lan" }, + "wired-down": { "prefix": "lan" }, + "tunnel-up": { "prefix": "tun" }, + "tunnel-down": { "prefix": "tun" } + }, + "battery": { + "charged": { "suffix": "full" }, + "charging": { "suffix": "chr" }, + "AC": { "suffix": "ac" }, + "discharging-10": { + "prefix": "!", + "suffix": "dis" + }, + "discharging-25": { "suffix": "dis" }, + "discharging-50": { "suffix": "dis" }, + "discharging-80": { "suffix": "dis" }, + "discharging-100": { "suffix": "dis" } + }, + "caffeine": { + "activated": {"prefix": "caf-on" }, "deactivated": { "prefix": "caf-off " } + }, + "xrandr": { + "on": { "prefix": " off "}, "off": { "prefix": " on "} + } +} From c3214a8a3b8c784a9bb8a3eca56f99ba1029571f Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 13:05:49 +0100 Subject: [PATCH 103/104] [bin] Re-add i3bar load script --- bin/load-i3-bars.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100755 bin/load-i3-bars.sh diff --git a/bin/load-i3-bars.sh b/bin/load-i3-bars.sh new file mode 100755 index 0000000..cbe0e95 --- /dev/null +++ b/bin/load-i3-bars.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +if [ ! -f ~/.i3/config.template ]; then + cp ~/.i3/config ~/.i3/config.template +else + cp ~/.i3/config.template ~/.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 ~/.i3/config.$screen-$others ]; then + cat ~/.i3/config.$screen-$others >> ~/.i3/config + else + if [ -f ~/.i3/config.$screen ]; then + cat ~/.i3/config.$screen >> ~/.i3/config + fi + fi +done <<< "$screens" + +i3-msg restart From 9b9403d92ee75255192af244dbbb6600b7f37b5a Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sun, 11 Dec 2016 13:08:07 +0100 Subject: [PATCH 104/104] [doc] Update screenshot for CPU module --- screenshots/cpu.png | Bin 1811 -> 2003 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/cpu.png b/screenshots/cpu.png index f160ba20c5eca154debfc010225187c556456284..0b5527c8d0d370b852907d42808b0987f7aa902d 100644 GIT binary patch delta 1982 zcmV;v2SNCg4$}`IiBL{Q4GJ0x0000DNk~Le0001Z0000Q2m$~A0P{Kz)R7@Ae+O?# zL_t(&-tAglP#f13zFNupK?3wIAp}A~fTBNSz!-r*IJiJ?Z0y)cX-ZN%X_HQ7+G#WG zv`_W4$wOcI(wDRyHywKt$E_Xmhl42?+hD*nN-PYBUmIZ=fj~%FBqSlB-98XxA%S+8 zX?bY%JxS->v)}Hw_nvd^l?Xspe-2Xk?}OA*XT&q^%FR0|vG$5sZK0GL`!u65M{I0F ztPV#?slCZcBIXGMdk`8ykiL;II+c>r4nH6Ws@AC1g6rFiNdiy+1}zf~mpi2$eh>iA zD-{AZD_;F1G2cwi*c{6#?e>I{B6*I0w}%`(`e<(Y$vocINFYEE6p2Lee+*`2q<^oW zR+gP*wJ%=oyyb$ucw7Vmp|P^8vRFkXktVE<+j{Q8K0hW`zBuc}qcwasb9R35?_Ir1 zF{^VouBNK;f?N^o^Iy8&jUY%#k-VYm(C`0t1w~OzI<-tvAkN;=MN}WIO0>?*+s!kN zp59YB!|nAY4x>`YN6Ly{f2cE@sWTiil-HIP=VuEe2;y*gcDofK1pTDtx5C?e}6QBAW68gxa@j;=^sA$SYNC}5M;^a`QfSOKIyr;?1nK_f0k#9bOrg*uWql8 zmCi_(I&6+*)5J99b&Dk8^KZ1CFzO{j{=Jd$-@JDb_WIOv>FdpnKYRI9CL?XfsB*O; z91gel4SCo69RsFx2K}%`f$8EhGxb{KpW3cl?G6|0{iyS{(}Vrwm0IQO{NnAeAGp1~ zYd8DC;f*p?QF1N;f1A0w<_7?{VXuJAG8|O9;8n})V@$3b0k2F^kgP(+eRsG$gO&-* z>niec-Z*`1bjG^5;!qSsSJt%6z8(ty{QIXFG+JU{sX{(DGO;yDs|hI3%B7etr7YLF z;E2s6cp%^~7o4td3pqeL;aS6P-+cA$*3&AP1dq^xV2G7Yk8M@c z>9pir4wt*fe`Nmkxt7O{Wj>oVZnZtZ(GIAkc@PBcP3S7Ma>tn%3uCF4PNT|mL@zfU zX{adOH2MO8KVJC^LD6$3>R)SaBoGM21u`+88yCx~SzzrnCjJB8eCPf1r`)RTZm9TXH1f>iO6G^MAh7 z(LZF+soML7@VwFfQHw7S*xSyKN+#DH)O4H7!BA+o7;S-Uv!3X+%BXQ-)+QHcK?Fjf zL>xQv$&xcRc0C*x@iK&;Ia;QplF576 zf-`6|qfXQFjXAurbGyc4W?pRXa=6^TXgw2MGUKz471G?z&vT1Q7uvpjzNUiDX2!)X zEW2L%j&X2g++>*o0B|zWm$%AV5|`KSr&69if5U>)r7e)+nrp+Bd!v(7@`1HLu(hcU z#=!*u5OUdSx%8dCe9&yv>lE@iyW_)-o4ctIUC?*TBhQxT4v>hrQb)9(tJZ3E%R3PS zLD6k(<;C_c)A;n$J{m<)fC3cVNa(E1Qx5?EpKn!|;==r~hjW;&8QVgUB!{?lKkN*K ze{0o;vm{0$k+ndOL?kNma>uPUOfCRGvr+%gL6eZjQRnBqd*L#XNUYE(F^Sw>U*Cg~ z-74I!DV)#BtW+xq1VW-~+3h)dy#7}|cxm%Zc)UK7WeP!%ds}r}k|lWet#gMp&nzW0 z)PoN!u+R71dLy6BYN{&F5%TVuM=@Q)e^U=b;c#nH9haH;j5JqYtQ>q`iM?!nMd|N; z{3@538Eed7&`%zz5^`ACeD1ju4T~$T$t|IiaLIE-LJq6XJO%&=ghG)>1orx%opeaV zk{ccvo!D_}v-5`K+Z(0RsbsR!A>W<9y(M72M3sqh-219Q<#_#96`{ zk>pi41t$#SJtj@zKly7wN`n3RK_K;agM9&sLT98@*pC?<@!L`=>;wJ{6nxOu;5aA` Qj{pDw07*qoM6N<$g6M?lY5)KL delta 1788 zcmVyaTXe+I8f zL_t(&-tAdgP*YbJKKCYMfrO9{5(tC(o)lI%ARQh@-6wDo{Y&fItul1P~JmF?&ctmV5gUSxm^iv2{ArHs8~| z=llQvocrDLpYz{yA&^xJ!gxU-f8tuava&oZ)1o-l+~U^)hei2^<|dIyNa1qB8U@1R z@s4S=&!Hv^!(};H)6SW&c78w*q^(a8MIkRD+M*5+0413cE{hq~&JQR6odbhfgK_aG zxM*uZx{%L}3u_mMdk=;uw7Tb5H?z|OqNKzSqeBoxfFKWogksE!j@nrxe@jc^jH;)8 zzSvcWboTm~>j88-+}&D$dxtx_H~_;`J-?E0<<2 zN#b}BM60(1?TG-tzjN)-fB560O|2{jZBs=-N4FB0eTJ9Uyp**h>FBw(Yh8*IUfh^Q zXLq`=pA92mFe-r^vsRXS==cShRP03&t-~ zrt`URMG_HiTuDm8XM5{vD~kjP>`wXc{%=m3tkXFnetli#2fL~nfAq*jy_RJQU2gCB z)^6*x^I}_XbR>0|ROqAPm~@#`^nF9~m`0~J+fH8W&>5|`!@vj_jN-Bx7MlYAU@+Ub zEJj&=y1`@}7*XT)HKEnr8C2q$&8x{}#*}1A@TE-Uy|ryszjDHtJ17d~GGyrCq|5F3 z@U?1sBqh+XC^My_e^)s>6N@1TNJV_#ZB|;MN~2qtr=b8NU@+>j&XmYuQYob5M7G{! zt(Hm8UcZamk=AJH92g8`Nb_oDr>4L#PB9-zA?>LxSE;8?TxkETT@C;!FO+<-Z=2ig z1ppj5bK~%tYq1R4j_Q&?UoM+5WiSH(N~L14fY)&ORw9Sxf1`MDOp|4L>#E{|?`-|( zjhbvBKa`WN7`{&G1whJ>~;%*)r_KVWk>u&pbX zW<*iRB0hK5y3)0>TwkNr?)>`X?+AkIt}UJEkxC}{e<&)IlrIqw2=KS3uC#X#)z_6L$Fp&Vju8-pYVJ~e`d#CNKYErIh|jm& z3q>PYb|#HV#WiCyqUY6gx?DH9`5eFbt=sBxEj0 zTxhuvPM0T%!>X^VXt>m+o-&dnh%UG1=_`fc!Li9-e=gme)SElx!(H-`vH~&g=rICf zP(Ex}k=Nd>G+3rXQIkRXQuLe*W-_$eDcldn|&i(R?$aa*AR0K~@7re=3itKH#P z@X_^He{W{T&~YCUBOnF^07ziRta6&`qd>+aeWL?Qt%52)pNIT69liZaDa zI?aEfG+J$K^1fipgcm^&6h)!=mSat=cN8k`oIMIqfTAcGSb_EojsXD9U4NQfdFcYh z$Rppia$GA(;SmV%T&buJVb)*-#Gn8GwN*uze>&s|Y(}<_f8d8RM3_*Ti(knw>9n%k zoM2na9mNBq#kvSLWi)s64=!5G>I|0Mo60}ly@3Ej005KKeyd;QMbM7kVV{A3$NqBP z_L5xTl-}&;+uo}*+w424OSmk?y0ZM_gxJ=5PdCb*q462F=k@i=tMEv;pSVrsJK>&br#^pf}lg&Q&BC)|?GD@>05CjEMMyu`6 ziAMkG-3N=^KI3xn*s*@qJB><~%qBGefJ`D9%(iKVtEMc^Z`*0O+-0*npFJ!<`-Ucp zv(q+K6v8m{)7ci&?AH#8qW!}Yg&C=}e^rHPNu0(jw+B?u)PU(J@o7n%&c2ap$4n>z zF{t{wii_=Xwa#d=Iubdo6@?j*WKvVhJ+C*UT{}7|ib5i3p6COscbD4@VVu2Zr3#`b zlr&!aTnm*#I e6fLa6i^88=1#8g&aSN>g0000