diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d95d41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +#start with a Python 3.x container +FROM python:3 + +#grab repository from github +RUN git clone --recursive https://github.com/tobi-wan-kenobi/bumblebee-status.git /var/bumblebee-status +#run the statusline with no modules or themes specified +CMD python3 /var/bumblebee-status/bumblebee-status diff --git a/README.md b/README.md index 636e733..4c6d258 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) -**Many, many thanks to all contributors! As of now, 47 of the modules are from various contributors (!), and only 19 from myself.** +**Many, many thanks to all contributors! As of now, 48 of the modules are from various contributors (!), and only 19 from myself.** ![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png) diff --git a/bumblebee/engine.py b/bumblebee/engine.py index 6c74f6a..1ded7c5 100644 --- a/bumblebee/engine.py +++ b/bumblebee/engine.py @@ -25,7 +25,7 @@ def all_modules(): "name": mod }) return result - + class Module(object): """Module instance base class @@ -142,7 +142,7 @@ class Engine(object): self._running = True self._modules = [] self.input = inp - self._aliases = self._read_aliases() + self._aliases = self._aliases() self.load_modules(config.modules()) self._current_module = None self._theme = theme @@ -230,16 +230,16 @@ class Engine(object): self.input.register_callback(obj=module, button=button["id"], cmd=module.parameter(button["name"])) - def _read_aliases(self): - result = {} - for module in all_modules(): - try: - mod = importlib.import_module("bumblebee.modules.{}".format(module["name"])) - for alias in getattr(mod, "ALIASES", []): - result[alias] = module["name"] - except Exception as error: - log.warning("failed to import {}: {}".format(module["name"], str(error))) - return result + def _aliases(self): + return { + 'date': 'datetime', + 'time': 'datetime', + 'datetz': 'datetimetz', + 'timetz': 'datetimetz', + 'pasink': 'pulseaudio', + 'pasource': 'pulseaudio', + 'test-alias': 'test', + } def _load_module(self, module_name, config_name=None): """Load specified module and return it as object""" diff --git a/bumblebee/modules/caffeine.py b/bumblebee/modules/caffeine.py index 2354ed0..ed9633c 100644 --- a/bumblebee/modules/caffeine.py +++ b/bumblebee/modules/caffeine.py @@ -1,12 +1,17 @@ -# pylint: disable=C0111,R0903 +#pylint: disable=C0111,R0903,W0212 """Enable/disable automatic screen locking. Requires the following executables: - * xset + * xdg-screensaver + * xdotool + * xprop (as dependency for xdotool) * notify-send """ +import logging +import os +import psutil import bumblebee.input import bumblebee.output import bumblebee.engine @@ -14,35 +19,84 @@ 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) + bumblebee.output.Widget(full_text="") ) + self._active = False + self._xid = None + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._toggle ) - def caffeine(self, widget): - return "" + def _check_requirements(self): + requirements = ['xdotool', 'xprop', 'xdg-screensaver'] + missing = [] + for tool in requirements: + if not bumblebee.util.which(tool): + missing.append(tool) + return missing - def state(self, widget): - if self._active(): + def _get_i3bar_xid(self): + xid = bumblebee.util.execute("xdotool search --class \"i3bar\"").partition('\n')[0].strip() + if xid.isdigit(): + return xid + logging.warning("Module caffeine: xdotool couldn't get X window ID of \"i3bar\".") + return None + + def _notify(self): + if not bumblebee.util.which('notify-send'): + return + + if self._active: + bumblebee.util.execute("notify-send \"Consuming caffeine\"") + else: + bumblebee.util.execute("notify-send \"Out of coffee\"") + + def _suspend_screensaver(self): + self._xid = self._get_i3bar_xid() + if self._xid is None: + return False + + pid = os.fork() + if pid == 0: + os.setsid() + bumblebee.util.execute("xdg-screensaver suspend {}".format(self._xid)) + os._exit(0) + else: + os.waitpid(pid, 0) + return True + + def _resume_screensaver(self): + success = True + xprop_path = bumblebee.util.which('xprop') + pids = [ p.pid for p in psutil.process_iter() if p.cmdline() == [xprop_path, '-id', str(self._xid), '-spy'] ] + for pid in pids: + try: + os.kill(pid, 9) + except OSError: + success = False + return success + + def state(self, _): + if self._active: return "activated" return "deactivated" - def _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 _toggle(self, _): + missing = self._check_requirements() + if missing: + logging.warning('Could not run caffeine - missing %s!', ", ".join(missing)) + return - def _toggle(self, widget): - if self._active(): - bumblebee.util.execute("xset s default") - bumblebee.util.execute("notify-send \"Out of coffee\"") + self._active = not self._active + if self._active: + success = self._suspend_screensaver() else: - bumblebee.util.execute("xset s off") - bumblebee.util.execute("notify-send \"Consuming caffeine\"") + success = self._resume_screensaver() + + if success: + self._notify() + else: + self._active = not self._active # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/datetime.py b/bumblebee/modules/datetime.py index e9afeb2..8899c4a 100644 --- a/bumblebee/modules/datetime.py +++ b/bumblebee/modules/datetime.py @@ -16,8 +16,6 @@ import datetime import locale import bumblebee.engine -ALIASES = ["date", "time"] - def default_format(module): default = "%x %X" if module == "date": diff --git a/bumblebee/modules/datetimetz.py b/bumblebee/modules/datetimetz.py index e298249..1d0e3cb 100644 --- a/bumblebee/modules/datetimetz.py +++ b/bumblebee/modules/datetimetz.py @@ -26,8 +26,6 @@ import bumblebee.input import bumblebee.output import bumblebee.engine -ALIASES = ["datetz", "timetz"] - def default_format(module): default = "%x %X %Z" if module == "datetz": diff --git a/bumblebee/modules/deadbeef.py b/bumblebee/modules/deadbeef.py index 2dce7fd..5b74933 100644 --- a/bumblebee/modules/deadbeef.py +++ b/bumblebee/modules/deadbeef.py @@ -1,22 +1,38 @@ # pylint: disable=C0111,R0903 -"""Displays the current song being played in DeaDBeeF and -provides some media control bindings. -Left click toggles pause, scroll up skips the current song, -scroll down returns to the previous song. +"""Displays the current song being played in DeaDBeeF and provides +some media control bindings. +Left click toggles pause, scroll up skips the current song, scroll +down returns to the previous song. + Requires the following library: * subprocess Parameters: - * deadbeef.format: Format string (defaults to "{artist} - {title}") - Available values are: {artist}, {title}, {album}, {length}, - {trackno}, {year}, {comment}, - {copyright}, {time} - * deadbeef.previous: Change binding for previous song (default is left click) - * deadbeef.next: Change binding for next song (default is right click) - * deadbeef.pause: Change binding for toggling pause (default is middle click) + * deadbeef.format: Format string (defaults to "{artist} - {title}") + Available values are: {artist}, {title}, {album}, {length}, + {trackno}, {year}, {comment}, + {copyright}, {time} + This is deprecated, but much simpler. + * deadbeef.tf_format: A foobar2000 title formatting-style format string. + These can be much more sophisticated than the standard + format strings. This is off by default, but specifying + any tf_format will enable it. If both deadbeef.format + and deadbeef.tf_format are specified, deadbeef.tf_format + takes priority. + * deadbeef.tf_format_if_stopped: Controls whether or not the tf_format format + string should be displayed even if no song is paused or + playing. This could be useful if you want to implement + your own stop strings with the built in logic. Any non- + null value will enable this (by default the module will + hide itself when the player is stopped). + * deadbeef.previous: Change binding for previous song (default is left click) + * deadbeef.next: Change binding for next song (default is right click) + * deadbeef.pause: Change binding for toggling pause (default is middle click) Available options for deadbeef.previous, deadbeef.next and deadbeef.pause are: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN + """ +import sys import bumblebee.input import bumblebee.output @@ -34,54 +50,94 @@ class Module(bumblebee.engine.Module): super(Module, self).__init__(engine, config, bumblebee.output.Widget(full_text=self.deadbeef) ) - buttons = {"LEFT_CLICK":bumblebee.input.LEFT_MOUSE, - "RIGHT_CLICK":bumblebee.input.RIGHT_MOUSE, - "MIDDLE_CLICK":bumblebee.input.MIDDLE_MOUSE, - "SCROLL_UP":bumblebee.input.WHEEL_UP, - "SCROLL_DOWN":bumblebee.input.WHEEL_DOWN, + buttons = {"LEFT_CLICK": bumblebee.input.LEFT_MOUSE, + "RIGHT_CLICK": bumblebee.input.RIGHT_MOUSE, + "MIDDLE_CLICK": bumblebee.input.MIDDLE_MOUSE, + "SCROLL_UP": bumblebee.input.WHEEL_UP, + "SCROLL_DOWN": bumblebee.input.WHEEL_DOWN, } self._song = "" self._format = self.parameter("format", "{artist} - {title}") + self._tf_format = self.parameter("tf_format", "") + self._show_tf_when_stopped = bool(self.parameter("tf_format_if_stopped", "")) prev_button = self.parameter("previous", "LEFT_CLICK") next_button = self.parameter("next", "RIGHT_CLICK") pause_button = self.parameter("pause", "MIDDLE_CLICK") - self.now_playing = ["deadbeef","--nowplaying","%a;%t;%b;%l;%n;%y;%c;%r;%e"] + self.now_playing = ["deadbeef", "--nowplaying", "%a;%t;%b;%l;%n;%y;%c;%r;%e"] + self.now_playing_tf = ["deadbeef", "--nowplaying-tf", ""] cmd = "deadbeef " - + engine.input.register_callback(self, button=buttons[prev_button], - cmd=cmd + "--prev") + cmd=cmd + "--prev") engine.input.register_callback(self, button=buttons[next_button], - cmd=cmd + "--next") + cmd=cmd + "--next") engine.input.register_callback(self, button=buttons[pause_button], - cmd=cmd + "--play-pause") + cmd=cmd + "--play-pause") + + # modify the tf_format if we don't want it to show on stop + # this adds conditions to the query itself, rather than + # polling to see if deadbeef is running + # doing this reduces the number of calls we have to make + if self._tf_format and not self._show_tf_when_stopped: + self._tf_format = "$if($or(%isplaying%,%ispaused%),{query})".format(query=self._tf_format) @scrollable def deadbeef(self, widget): - return str(self._song) + return self.string_song def hidden(self): - return str(self._song) == "" + return self.string_song == "" def update(self, widgets): try: - deadbeef = subprocess.Popen(self.now_playing,stdin=subprocess.PIPE,stdout=subprocess.PIPE) - data = deadbeef.communicate()[0] - if data == "nothing": - self._song = "" - else: - data = data.split(";") - self._song = self._format.format(artist=data[0], - title=data[1], - album=data[2], - length=data[3], - trackno=data[4], - year=data[5], - comment=data[6], - copyright=data[7], - time=data[8]) + if self._tf_format == "": # no tf format set, use the old style + return self.update_standard(widgets) + return self.update_tf(widgets) except Exception: + self._song = "error" + + def update_tf(self, widgets): + ## ensure that deadbeef is actually running + ## easiest way to do this is to check --nowplaying for + ## the string "nothing" + if read_process(self.now_playing) == "nothing": self._song = "" + return + ## perform the actual query -- these can be much more sophisticated + self.now_playing_tf[-1] = self._tf_format + data = read_process(self.now_playing_tf) + self._song = data + + def update_standard(self, widgets): + data = read_process(self.now_playing) + if data == "nothing": + self._song = "" + else: + data = data.split(";") + self._song = self._format.format(artist=data[0], + title=data[1], + album=data[2], + length=data[3], + trackno=data[4], + year=data[5], + comment=data[6], + copyright=data[7], + time=data[8]) + + @property + def string_song(self): + """\ +Returns the current song as a string, either as a unicode() (Python < +3) or a regular str() (Python >= 3) + """ + if sys.version_info.major < 3: + return unicode(self._song) + return str(self._song) + +def read_process(command): + proc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + return proc.stdout.read() # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/pomodoro.py b/bumblebee/modules/pomodoro.py new file mode 100644 index 0000000..64f1514 --- /dev/null +++ b/bumblebee/modules/pomodoro.py @@ -0,0 +1,119 @@ +# pylint: disable=C0111,R0903 + +"""Display and run a Pomodoro timer. +Left click to start timer, left click again to pause. +Right click will cancel the timer. + +Parameters: + * pomodoro.work: The work duration of timer in minutes (defaults to 25) + * pomodoro.break: The break duration of timer in minutes (defaults to 5) + * pomodoro.format: Timer display format with "%m" and "%s" for minutes and seconds (defaults to "%m:%s") + Examples: "%m min %s sec", "%mm", "", "timer" + * pomodoro.notify: Notification command to run when timer ends/starts (defaults to nothing) + Example: 'notify-send "Time up!"' +""" + +from __future__ import absolute_import +import datetime +from math import ceil + +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +class Module(bumblebee.engine.Module): + def __init__(self, engine, config): + widgets = bumblebee.output.Widget(full_text=self.text) + + super(Module, self).__init__(engine, config, widgets) + + # Parameters + self._work_period = int(self.parameter("work", 25)) + self._break_period = int(self.parameter("break", 5)) + self._time_format = self.parameter("format", "%m:%s") + self._notify_cmd = self.parameter("notify", "") + + # TODO: Handle time formats more gracefully. This is kludge. + self.display_seconds_p = False + self.display_minutes_p = False + if "%s" in self._time_format: + self.display_seconds_p = True + if "%m" in self._time_format: + self.display_minutes_p = True + + self.remaining_time = datetime.timedelta(minutes=self._work_period) + + self.time = None + self.pomodoro = { "state":"OFF", "type": ""} + self._text = self.remaining_time_str() + self.pomodoro["type"] + + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd=self.timer_play_pause) + engine.input.register_callback(self, button=bumblebee.input.RIGHT_MOUSE, + cmd=self.timer_reset) + + def remaining_time_str(self): + + if self.display_seconds_p and self.display_minutes_p: + minutes, seconds = divmod(self.remaining_time.seconds, 60) + if not self.display_seconds_p: + minutes = ceil(self.remaining_time.seconds / 60) + seconds = 0 + if not self.display_minutes_p: + minutes = 0 + seconds = self.remaining_time.seconds + + minutes = "{:2d}".format(minutes) + seconds = "{:02d}".format(seconds) + return self._time_format.replace("%m",minutes).replace("%s",seconds)+" " + + def text(self, widget): + return "{}".format(self._text) + + def update(self, widget): + if self.pomodoro["state"] == "ON": + timediff = (datetime.datetime.now() - self.time) + if timediff.seconds >= 0: + self.remaining_time -= timediff + self.time = datetime.datetime.now() + + if self.remaining_time.seconds <= 0: + self.notify() + if self.pomodoro["type"] == "Work": + self.pomodoro["type"] = "Break" + self.remaining_time = datetime.timedelta(minutes=self._break_period) + elif self.pomodoro["type"] == "Break": + self.pomodoro["type"] = "Work" + self.remaining_time = datetime.timedelta(minutes=self._work_period) + + self._text = self.remaining_time_str() + self.pomodoro["type"] + + def notify(self): + if self._notify_cmd: + bumblebee.util.execute(self._notify_cmd) + + def timer_play_pause(self, widget): + if self.pomodoro["state"] == "OFF": + self.pomodoro = {"state": "ON", "type": "Work"} + self.remaining_time = datetime.timedelta(minutes=self._work_period) + self.time = datetime.datetime.now() + elif self.pomodoro["state"] == "ON": + self.pomodoro["state"] = "PAUSED" + self.remaining_time -= (datetime.datetime.now() - self.time) + self.time = datetime.datetime.now() + elif self.pomodoro["state"] == "PAUSED": + self.pomodoro["state"] = "ON" + self.time = datetime.datetime.now() + + def timer_reset(self, widget): + if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "PAUSED": + self.pomodoro = {"state":"OFF", "type": "" } + self.remaining_time = datetime.timedelta(minutes=self._work_period) + + def state(self, widget): + state = []; + state.append(self.pomodoro["state"].lower()) + if self.pomodoro["state"] == "ON" or self.pomodoro["state"] == "OFF": + state.append(self.pomodoro["type"].lower()) + + return state diff --git a/bumblebee/modules/pulseaudio.py b/bumblebee/modules/pulseaudio.py index 8b2f276..5cdd23c 100644 --- a/bumblebee/modules/pulseaudio.py +++ b/bumblebee/modules/pulseaudio.py @@ -24,8 +24,6 @@ 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, diff --git a/bumblebee/modules/rss.py b/bumblebee/modules/rss.py index c2db30b..2a27d27 100644 --- a/bumblebee/modules/rss.py +++ b/bumblebee/modules/rss.py @@ -45,6 +45,7 @@ class Module(bumblebee.engine.Module): # Use BBC newsfeed as demo: self._feeds = self.parameter('feeds', 'https://www.espn.com/espn/rss/news').split(" ") self._feeds_to_update = [] + self._response = "" self._max_title_length = int(self.parameter("length", 60)) diff --git a/bumblebee/modules/spotify.py b/bumblebee/modules/spotify.py index 5f4bcee..abc12bd 100644 --- a/bumblebee/modules/spotify.py +++ b/bumblebee/modules/spotify.py @@ -13,6 +13,8 @@ Parameters: LEFT_CLICK, RIGHT_CLICK, MIDDLE_CLICK, SCROLL_UP, SCROLL_DOWN """ +import sys + import bumblebee.input import bumblebee.output import bumblebee.engine @@ -52,12 +54,12 @@ class Module(bumblebee.engine.Module): engine.input.register_callback(self, button=buttons[pause_button], cmd=cmd + "PlayPause") -## @scrollable + @scrollable def spotify(self, widget): - return str(self._song) + return self.string_song def hidden(self): - return str(self._song) == "" + return self.string_song == "" def update(self, widgets): try: @@ -71,7 +73,15 @@ class Module(bumblebee.engine.Module): artist=','.join(props.get('xesam:artist')), trackNumber=str(props.get('xesam:trackNumber')), playbackStatus=u"\u25B6" if playback_status=="Playing" else u"\u258D\u258D" if playback_status=="Paused" else "",) + except Exception: self._song = "" + @property + def string_song(self): + if sys.version_info.major < 3: + return unicode(self._song) + return str(self._song) + + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/modules/test.py b/bumblebee/modules/test.py index b654575..5e92e0e 100644 --- a/bumblebee/modules/test.py +++ b/bumblebee/modules/test.py @@ -5,8 +5,6 @@ import bumblebee.engine -ALIASES = ["test-alias"] - class Module(bumblebee.engine.Module): def __init__(self, engine, config): super(Module, self).__init__(engine, config, diff --git a/bumblebee/modules/vault.py b/bumblebee/modules/vault.py index 9e1535e..6505dc2 100644 --- a/bumblebee/modules/vault.py +++ b/bumblebee/modules/vault.py @@ -60,7 +60,7 @@ class Module(bumblebee.engine.Module): def _reset(self): self._timer = None - self._text = "" + self._text = str(self.parameter("text", "")) def _callback(self, secret_name): secret_name = secret_name.replace(self._path, "") # remove common path diff --git a/bumblebee/modules/zpool.py b/bumblebee/modules/zpool.py index 701c1c1..1f3e897 100644 --- a/bumblebee/modules/zpool.py +++ b/bumblebee/modules/zpool.py @@ -25,9 +25,12 @@ Be aware of security implications of doing this! """ import time +import logging +from pkg_resources import parse_version import bumblebee.engine from bumblebee.util import execute, bytefmt, asbool +log = logging.getLogger(__name__) class Module(bumblebee.engine.Module): def __init__(self, engine, config): @@ -63,7 +66,16 @@ class Module(bumblebee.engine.Module): return state def _update_widgets(self, widgets): + zfs_version_path = "/sys/module/zfs/version" # zpool list -H: List all zpools, use script mode (no headers and tabs as separators). + try: + with open(zfs_version_path, 'r') as zfs_mod_version: + zfs_version = zfs_mod_version.readline().rstrip().split('-')[0] + except IOError: + # ZFS isn't installed or the module isn't loaded, stub the version + zfs_version = "0.0.0" + logging.error("ZFS version information not found at {}, check the module is loaded.".format(zfs_version_path)) + raw_zpools = execute(('sudo ' if self._usesudo else '') + 'zpool list -H').split('\n') for widget in widgets: @@ -71,8 +83,11 @@ class Module(bumblebee.engine.Module): for raw_zpool in raw_zpools: try: - # Ignored fields (assigned to _) are "expandsz" and "altroot" - name, size, alloc, free, _, frag, cap, dedup, health, _ = raw_zpool.split('\t') + # Ignored fields (assigned to _) are "expandsz" and "altroot", also "ckpoint" in ZFS 0.8.0+ + if parse_version(zfs_version) < parse_version("0.8.0"): + name, size, alloc, free, _, frag, cap, dedup, health, _ = raw_zpool.split('\t') + else: + name, size, alloc, free, _, _, frag, cap, dedup, health, _ = raw_zpool.split('\t') cap = cap.rstrip('%') percentuse=int(cap) percentfree=100-percentuse diff --git a/bumblebee/output.py b/bumblebee/output.py index 5c6ff92..f70b605 100644 --- a/bumblebee/output.py +++ b/bumblebee/output.py @@ -8,12 +8,15 @@ import uuid import bumblebee.store +_TrueValues = ["true", "t", "yes", "y", "1"] + def scrollable(func): def wrapper(module, widget): text = func(module, widget) if not text: return text width = widget.get("theme.width", module.parameter("width", 30)) - widget.set("theme.minwidth", "A"*width) + if module.parameter("scrolling.makewide", "true").lower() in _TrueValues: + widget.set("theme.minwidth", "A"*width) if len(text) <= width: return text # we need to shorten diff --git a/bumblebee/util.py b/bumblebee/util.py index f3545ac..fdd3af5 100644 --- a/bumblebee/util.py +++ b/bumblebee/util.py @@ -31,7 +31,7 @@ def execute(cmd, wait=True): raise RuntimeError("{} exited with {}".format(cmd, proc.returncode)) if hasattr(out, "decode"): - rv = out.decode("utf-8") + rv = out.decode("utf-8", "ignore") else: rv = out diff --git a/tests/modules/test_caffeine.py b/tests/modules/test_caffeine.py index e95e2ac..ad0cf00 100644 --- a/tests/modules/test_caffeine.py +++ b/tests/modules/test_caffeine.py @@ -1,17 +1,9 @@ # pylint: disable=C0103,C0111 -import json import unittest -import mock - -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - +from mock import patch import tests.mocks as mocks -from bumblebee.config import Config from bumblebee.input import LEFT_MOUSE from bumblebee.modules.caffeine import Module @@ -19,35 +11,40 @@ class TestCaffeineModule(unittest.TestCase): def setUp(self): mocks.setup_test(self, Module) - self.xset_active = " timeout: 0 cycle: 123" - self.xset_inactive = " timeout: 600 cycle: 123" - def tearDown(self): mocks.teardown_test(self) - def test_text(self): - self.assertEquals(self.module.caffeine(self.anyWidget), "") + def test_check_requirements(self): + with patch('bumblebee.util.which', side_effect=['', 'xprop', 'xdg-screensaver']): + self.assertTrue(['xdotool'] == self.module._check_requirements()) - def test_active(self): - self.popen.mock.communicate.return_value = (self.xset_active, None) - self.assertTrue(not "deactivated" in self.module.state(self.anyWidget)) - self.assertTrue("activated" in self.module.state(self.anyWidget)) + def test_get_i3bar_xid_returns_digit(self): + self.popen.mock.communicate.return_value = ("8388614", None) + self.assertTrue(self.module._get_i3bar_xid().isdigit()) - def test_inactive(self): - self.popen.mock.communicate.return_value = (self.xset_inactive, None) - self.assertTrue("deactivated" in self.module.state(self.anyWidget)) - self.popen.mock.communicate.return_value = ("no text", None) - self.assertTrue("deactivated" in self.module.state(self.anyWidget)) + def test_get_i3bar_xid_returns_error_string(self): + self.popen.mock.communicate.return_value = ("Some error message", None) + self.assertTrue(self.module._get_i3bar_xid() is None) - def test_toggle(self): - self.popen.mock.communicate.return_value = (self.xset_active, None) - mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) - self.popen.assert_call("xset s default") - self.popen.assert_call("notify-send \"Out of coffee\"") - - self.popen.mock.communicate.return_value = (self.xset_inactive, None) - mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) - self.popen.assert_call("xset s off") - self.popen.assert_call("notify-send \"Consuming caffeine\"") + def test_get_i3bar_xid_returns_empty_string(self): + self.popen.mock.communicate.return_value = ("", None) + self.assertTrue(self.module._get_i3bar_xid() is None) + + def test_suspend_screensaver_success(self): + with patch.object(self.module, '_get_i3bar_xid', return_value=8388614): + mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) + self.assertTrue(self.module._suspend_screensaver() is True) + + def test_suspend_screensaver_fail(self): + with patch.object(self.module, '_get_i3bar_xid', return_value=None): + self.module._active = False + mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) + self.assertTrue(self.module._suspend_screensaver() is False) + + def test_resume_screensaver(self): + with patch.object(self.module, '_check_requirements', return_value=[]): + self.module._active = True + mocks.mouseEvent(stdin=self.stdin, button=LEFT_MOUSE, inp=self.input, module=self.module) + self.assertTrue(self.module._resume_screensaver() is True) # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/firefox-dark-powerline.json b/themes/firefox-dark-powerline.json new file mode 100644 index 0000000..00e7d95 --- /dev/null +++ b/themes/firefox-dark-powerline.json @@ -0,0 +1,42 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#002b36", + "bg": "#b58900" + }, + "critical": { + "fg": "#002b36", + "bg": "#dc322f" + } + }, + "cycle": [ + { "fg": "#B1B1B3", "bg": "#474749" }, + { "fg": "#BEBEBE", "bg": "#323234" } + ], + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "pacman": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "pomodoro": { + "paused": { + "fg": "#002b36", + "bg": "#b58900" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } + + +} diff --git a/themes/greyish-powerline.json b/themes/greyish-powerline.json index 1cbaaf6..2e9d9b8 100644 --- a/themes/greyish-powerline.json +++ b/themes/greyish-powerline.json @@ -42,5 +42,20 @@ "fg": "#002b36", "bg": "#859900" } - } + }, + "pomodoro": { + "paused": { + "fg": "#002b36", + "bg": "#b58900" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } + } diff --git a/themes/gruvbox-light.json b/themes/gruvbox-light.json index ebb7983..ee4d69e 100644 --- a/themes/gruvbox-light.json +++ b/themes/gruvbox-light.json @@ -54,5 +54,20 @@ "modified": { "bg": "#458588" }, "deleted": { "bg": "#9d0006" }, "new": { "bg": "#b16286" } - } + }, + "pomodoro": { + "paused": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } + } diff --git a/themes/gruvbox-powerline-light.json b/themes/gruvbox-powerline-light.json index ba4b441..d515df0 100644 --- a/themes/gruvbox-powerline-light.json +++ b/themes/gruvbox-powerline-light.json @@ -21,38 +21,52 @@ "fg": "#282828", "bg": "#fbf1c7" } - ], - "dnf": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - }, - "apt": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - }, - "battery": { - "charged": { - "fg": "#1d2021", - "bg": "#b8bb26" - }, - "AC": { - "fg": "#1d2021", - "bg": "#b8bb26" - } - }, - "bluetooth": { - "ON": { - "fg": "#1d2021", - "bg": "#b8bb26" - } - }, - "git": { - "modified": { "bg": "#458588" }, - "deleted": { "bg": "#9d0006" }, - "new": { "bg": "#b16286" } - } + ], + "dnf": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "apt": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "AC": { + "fg": "#1d2021", + "bg": "#b8bb26" + } + }, + "bluetooth": { + "ON": { + "fg": "#1d2021", + "bg": "#b8bb26" + } + }, + "git": { + "modified": { "bg": "#458588" }, + "deleted": { "bg": "#9d0006" }, + "new": { "bg": "#b16286" } + }, + "pomodoro": { + "paused": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } } diff --git a/themes/gruvbox-powerline.json b/themes/gruvbox-powerline.json index 1e0f588..3481dbc 100644 --- a/themes/gruvbox-powerline.json +++ b/themes/gruvbox-powerline.json @@ -54,5 +54,19 @@ "modified": { "bg": "#458588" }, "deleted": { "bg": "#9d0006" }, "new": { "bg": "#b16286" } - } + }, + "pomodoro": { + "paused": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } } diff --git a/themes/gruvbox.json b/themes/gruvbox.json index 8ef2813..ab71814 100644 --- a/themes/gruvbox.json +++ b/themes/gruvbox.json @@ -54,5 +54,20 @@ "modified": { "bg": "#458588" }, "deleted": { "bg": "#9d0006" }, "new": { "bg": "#b16286" } - } + }, + "pomodoro": { + "paused": { + "fg": "#1d2021", + "bg": "#d79921" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } + } diff --git a/themes/iceberg-dark-powerline.json b/themes/iceberg-dark-powerline.json index 81af90e..1b13b84 100644 --- a/themes/iceberg-dark-powerline.json +++ b/themes/iceberg-dark-powerline.json @@ -47,5 +47,20 @@ "fg": "#0f1117", "bg": "#84a0c6" } + }, + "pomodoro": { + "paused": { + "fg": "#0f1117", + "bg": "#e2a478" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#b4be82", + "bg": "#161821" + } } + } diff --git a/themes/iceberg-powerline.json b/themes/iceberg-powerline.json index f6af51b..c7dca23 100644 --- a/themes/iceberg-powerline.json +++ b/themes/iceberg-powerline.json @@ -46,5 +46,19 @@ "fg": "#0f1117", "bg": "#84a0c6" } + }, + "pomodoro": { + "paused": { + "fg": "#0f1117", + "bg": "#e2a478" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#89b8c2", + "bg": "#161821" + } } } diff --git a/themes/iceberg-rainbow.json b/themes/iceberg-rainbow.json index e8cef97..9c56af6 100644 --- a/themes/iceberg-rainbow.json +++ b/themes/iceberg-rainbow.json @@ -46,5 +46,20 @@ "fg": "#89b8c2", "bg": "#161821" } + }, + "pomodoro": { + "paused": { + "fg": "#e2a478", + "bg": "#c6c8d1" + }, + "work": { + "fg": "#89b8c2", + "bg": "#161821" + }, + "break": { + "fg": "#b4be82", + "bg": "#161821" + } } + } diff --git a/themes/iceberg.json b/themes/iceberg.json index 5739b83..9943eb4 100644 --- a/themes/iceberg.json +++ b/themes/iceberg.json @@ -40,5 +40,20 @@ "fg": "#89b8c2", "bg": "#161821" } + }, + "pomodoro": { + "paused": { + "fg": "#0f1117", + "bg": "#e2a478" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#89b8c2", + "bg": "#161821" + } } + } diff --git a/themes/icons/ascii.json b/themes/icons/ascii.json index 2084ca1..3b24f47 100644 --- a/themes/icons/ascii.json +++ b/themes/icons/ascii.json @@ -302,6 +302,10 @@ }, "system": { "prefix": "system" - } - + }, + "pomodoro": { + "off": { "prefix": "pom" }, + "paused": { "prefix": "||" }, + "on": { "prefix": " >" } + } } diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index eb805d4..d5e947f 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -222,5 +222,11 @@ }, "rss": { "prefix": "" + }, + "pomodoro": { + "off": { "prefix": "" }, + "paused": { "prefix": "" }, + "work": { "prefix": "" }, + "break": { "prefix": "" } } } diff --git a/themes/icons/ionicons.json b/themes/icons/ionicons.json index 08f9927..9f7a116 100644 --- a/themes/icons/ionicons.json +++ b/themes/icons/ionicons.json @@ -185,5 +185,11 @@ }, "rss": { "prefix": "\uf1ea" + }, + "pomodoro": { + "off": { "prefix": "\uf24f" }, + "paused": { "prefix": "\uf210" }, + "on": { "prefix": "\uf488" } } + } diff --git a/themes/onedark-powerline.json b/themes/onedark-powerline.json index 03867c6..d0ca586 100644 --- a/themes/onedark-powerline.json +++ b/themes/onedark-powerline.json @@ -36,5 +36,20 @@ "fg": "#282C34", "bg": "#98C379" } - } + }, + "pomodoro": { + "paused": { + "fg": "#282C34", + "bg": "#E5C07B" + }, + "work": { + "fg": "#98C379", + "bg": "#282C34" + }, + "break": { + "fg": "#282C34", + "bg": "#98C379" + } + } + } diff --git a/themes/powerline.json b/themes/powerline.json index 02f21f6..b2b603d 100644 --- a/themes/powerline.json +++ b/themes/powerline.json @@ -43,5 +43,20 @@ "fg": "#494949", "bg": "#41db00" } - } + }, + "pomodoro": { + "paused": { + "fg": "#d75f00", + "bg": "#ffd700" + }, + "work": { + "fg": "#ffd700", + "bg": "#d75f00" + }, + "break": { + "fg": "#494949", + "bg": "#41db00" + } + } + } diff --git a/themes/sac_red.json b/themes/sac_red.json index fade76e..7f6be68 100644 --- a/themes/sac_red.json +++ b/themes/sac_red.json @@ -40,5 +40,20 @@ }, "cmus": { "bg": "#C42021" - } + }, + "pomodoro": { + "paused": { + "fg": "#FDFFFC", + "bg": "#B91372" + }, + "work": { + "fg": "#FDFFFC", + "bg": "#41EAD4" + }, + "break": { + "fg": "#FDFFFC", + "bg": "#011627" + } + } + } diff --git a/themes/solarized-dark-awesome.json b/themes/solarized-dark-awesome.json index 9ec45e2..7274dc7 100644 --- a/themes/solarized-dark-awesome.json +++ b/themes/solarized-dark-awesome.json @@ -21,29 +21,43 @@ }, "apt": { "good": { - "fg": "#002b36", - "bg": "#859900" - } - }, - "pacman": { - "good": { - "fg": "#002b36", - "bg": "#859900" - } - }, - "battery": { - "charged": { - "fg": "#002b36", - "bg": "#859900" - }, - "AC": { - "fg": "#002b36", - "bg": "#859900" - } - }, - "git": { - "modified": { "bg": "#2aa198" }, - "deleted": { "bg": "#d33682" }, - "new": { "bg": "#859900" } - } + "fg": "#002b36", + "bg": "#859900" + } + }, + "pacman": { + "good": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "battery": { + "charged": { + "fg": "#002b36", + "bg": "#859900" + }, + "AC": { + "fg": "#002b36", + "bg": "#859900" + } + }, + "git": { + "modified": { "bg": "#2aa198" }, + "deleted": { "bg": "#d33682" }, + "new": { "bg": "#859900" } + }, + "pomodoro": { + "paused": { + "fg": "#002b36", + "bg": "#b58900" + }, + "work": { + "fg": "#eee8d5", + "bg": "#586e75" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } } diff --git a/themes/solarized-powerline.json b/themes/solarized-powerline.json index 3e52f7f..25854f5 100644 --- a/themes/solarized-powerline.json +++ b/themes/solarized-powerline.json @@ -47,5 +47,19 @@ "modified": { "bg": "#2aa198" }, "deleted": { "bg": "#d33682" }, "new": { "bg": "#859900" } - } + }, + "pomodoro": { + "paused": { + "fg": "#002b36", + "bg": "#b58900" + }, + "work": { + "fg": "#eee8d5", + "bg": "#586e75" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } } diff --git a/themes/solarized.json b/themes/solarized.json index 4486634..e6f0082 100644 --- a/themes/solarized.json +++ b/themes/solarized.json @@ -49,5 +49,20 @@ "modified": { "bg": "#2aa198" }, "deleted": { "bg": "#d33682" }, "new": { "bg": "#859900" } - } + }, + "pomodoro": { + "paused": { + "fg": "#002b36", + "bg": "#b58900" + }, + "work": { + "fg": "#eee8d5", + "bg": "#586e75" + }, + "break": { + "fg": "#002b36", + "bg": "#859900" + } + } + } diff --git a/themes/wal-powerline.json b/themes/wal-powerline.json index dcb4fa9..8b28d3a 100644 --- a/themes/wal-powerline.json +++ b/themes/wal-powerline.json @@ -44,5 +44,20 @@ "fg": "background", "bg": "color3" } - } + }, + "pomodoro": { + "paused": { + "fg": "cursor", + "bg": "color6" + }, + "work": { + "fg": "background", + "bg": "foreground" + }, + "break": { + "fg": "background", + "bg": "color3" + } + } + }