diff --git a/.codeclimate.yml b/.codeclimate.yml index d0c140e..ec80874 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -13,4 +13,3 @@ ratings: - "**.py" exclude_paths: - tests/ -- thirdparty/ diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 0000000..365c526 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,27 @@ +--- +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index 4996eaa..2bf1ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ ENV/ # Visual studio project files .vscode/ + +# mypy cache +.mypy_cache diff --git a/.travis.yml b/.travis.yml index 0e08cf9..78eff09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,17 +8,9 @@ python: - "3.8" before_install: - sudo apt-get -qq update - - sudo apt-get install -y task libdbus-1-dev install: - - pip install i3ipc - - pip install psutil - - pip install netifaces - - pip install -U coverage==4.3 + - pip install coverage - pip install codeclimate-test-reporter - - pip install taskw - - pip install pytz - - pip install tzlocal - - pip install dbus-python - pip install coverage script: - coverage run -m unittest discover -v diff --git a/bumblebee-status b/bumblebee-status index ad9c67f..9e4ea82 100755 --- a/bumblebee-status +++ b/bumblebee-status @@ -10,6 +10,7 @@ import logging import threading import bumblebee_status.discover + bumblebee_status.discover.discover() import core.config @@ -101,6 +102,7 @@ def main(): for module in config.modules(): modules.append(core.module.load(module, config, theme)) + modules[-1].register_callbacks() if config.reverse(): modules.reverse() diff --git a/bumblebee_status/__init__.py b/bumblebee_status/__init__.py index e69de29..8df5d95 100644 --- a/bumblebee_status/__init__.py +++ b/bumblebee_status/__init__.py @@ -0,0 +1,3 @@ +import bumblebee_status.discover + +bumblebee_status.discover.discover() diff --git a/bumblebee_status/core/config.py b/bumblebee_status/core/config.py index 82fcb6a..f779a14 100644 --- a/bumblebee_status/core/config.py +++ b/bumblebee_status/core/config.py @@ -27,7 +27,11 @@ THEME_HELP = "Specify the theme to use for drawing modules" def all_modules(): - """Return a list of available modules""" + """Returns a list of all available modules (either core or contrib) + + :return: list of modules + :rtype: list of strings + """ result = {} for path in [modules.core.__file__, modules.contrib.__file__]: @@ -127,6 +131,11 @@ class print_usage(argparse.Action): class Config(util.store.Store): + """Represents the configuration of bumblebee-status (either via config file or via CLI) + + :param args: The arguments passed via the commandline + """ + def __init__(self, args): super(Config, self).__init__() @@ -202,6 +211,11 @@ class Config(util.store.Store): key, value = param.split("=", 1) self.set(key, value) + """Loads parameters from an init-style configuration file + + :param filename: path to the file to load + """ + def load_config(self, filename): if os.path.exists(filename): log.info("loading {}".format(filename)) @@ -212,27 +226,75 @@ class Config(util.store.Store): for key, value in tmp.items("module-parameters"): self.set(key, value) + """Returns a list of configured modules + + :return: list of configured (active) modules + :rtype: list of strings + """ + def modules(self): return [item for sub in self.__args.modules for item in sub] + """Returns the global update interval + + :return: update interval in seconds + :rtype: float + """ + def interval(self, default=1): return util.format.seconds(self.get("interval", default)) + """Returns whether debug mode is enabled + + :return: True if debug is enabled, False otherwise + :rtype: boolean + """ + def debug(self): return self.__args.debug + """Returns whether module order should be reversed/inverted + + :return: True if modules should be reversed, False otherwise + :rtype: boolean + """ + def reverse(self): return self.__args.right_to_left + """Returns the logfile location + + :return: location where the logfile should be written + :rtype: string + """ + def logfile(self): return self.__args.logfile + """Returns the configured theme name + + :return: name of the configured theme + :rtype: string + """ + def theme(self): return self.__args.theme + """Returns the configured iconset name + + :return: name of the configured iconset + :rtype: string + """ + def iconset(self): return self.__args.iconset + """Returns which modules should be hidden if their state is not warning/critical + + :return: list of modules to hide automatically + :rtype: list of strings + """ + def autohide(self, name): return name in self.__args.autohide diff --git a/bumblebee_status/core/decorators.py b/bumblebee_status/core/decorators.py index 8c87308..8c60c70 100644 --- a/bumblebee_status/core/decorators.py +++ b/bumblebee_status/core/decorators.py @@ -1,5 +1,19 @@ +import difflib +import logging + import util.format +log = logging.getLogger(__name__) + + +"""Specifies that a module should never update (i.e. has static content). +This means that its update() method will never be invoked + +:param init: The __init__() method of the module + +:return: Wrapped method that sets the module's interval to "never" +""" + def never(init): def call_init(obj, *args, **kwargs): @@ -10,6 +24,16 @@ def never(init): return call_init +"""Specifies the interval for executing the module's update() method + +:param hours: Hours between two update() invocations, defaults to 0 +:param minutes: Minutes between two update() invocations, defaults to 0 +:param seconds: Seconds between two update() invocations, defaults to 0 + +:return: Wrapped method that sets the module's interval correspondingly +""" + + def every(hours=0, minutes=0, seconds=0): def decorator_init(init): def call_init(obj, *args, **kwargs): @@ -22,13 +46,30 @@ def every(hours=0, minutes=0, seconds=0): return decorator_init +"""Specifies that the module's content should scroll, if required + +The exact behaviour of this method is governed by a number of parameters, +specifically: The module's parameter "scrolling.width" specifies the width when +scrolling starts, "scrolling.makewide" defines whether the module should be expanded +to "scrolling.width" automatically, if the content is shorter, the parameter +"scrolling.bounce" defines whether it scrolls like a marquee (False) or should bounce +when the end of the content is reached. "scrolling.speed" defines the number of characters +to scroll each iteration. + +:param func: Function for which the result should be scrolled +""" + + def scrollable(func): def wrapper(module, widget): text = func(module, widget) if not text: return text - if text != widget.get("__content__", text): + if ( + difflib.SequenceMatcher(a=text, b=widget.get("__content__", text)).ratio() + < 0.9 + ): widget.set("scrolling.start", 0) widget.set("scrolling.direction", "right") widget.set("__content__", text) @@ -45,9 +86,10 @@ def scrollable(func): direction = widget.get("scrolling.direction", "right") if direction == "left": - scroll_speed = -scroll_speed - if start + scroll_speed <= 0: # bounce back + if start - scroll_speed < 0: # bounce back widget.set("scrolling.direction", "right") + else: + scroll_speed = -scroll_speed next_start = start + scroll_speed if next_start + width > len(text): diff --git a/bumblebee_status/core/input.py b/bumblebee_status/core/input.py index 66519d5..b775091 100644 --- a/bumblebee_status/core/input.py +++ b/bumblebee_status/core/input.py @@ -36,20 +36,20 @@ def __event_id(obj_id, button): return "{}::{}".format(obj_id, button_name(button)) -def __execute(cmd): +def __execute(cmd, wait=False): try: - util.cli.execute(cmd, wait=False) + util.cli.execute(cmd, wait=wait, shell=True) except Exception as e: logging.error("failed to invoke callback: {}".format(e)) -def register(obj, button=None, cmd=None): +def register(obj, button=None, cmd=None, wait=False): event_id = __event_id(obj.id if obj is not None else "", button) logging.debug("registering callback {}".format(event_id)) if callable(cmd): core.event.register(event_id, cmd) else: - core.event.register(event_id, lambda _: __execute(cmd)) + core.event.register(event_id, lambda _: __execute(cmd, wait)) def trigger(event): diff --git a/bumblebee_status/core/module.py b/bumblebee_status/core/module.py index 618cd6a..ba06848 100644 --- a/bumblebee_status/core/module.py +++ b/bumblebee_status/core/module.py @@ -2,10 +2,13 @@ import os import importlib import logging +import core.config import core.input import core.widget import core.decorators +import util.format + try: error = ModuleNotFoundError("") except Exception as e: @@ -14,6 +17,17 @@ except Exception as e: log = logging.getLogger(__name__) +"""Loads a module by name + +:param module_name: Name of the module to load +:param config: Configuration to apply to the module (defaults to an empty configuration) +:param theme: Theme for this module, defaults to None, which means whatever is configured in "config" + +:return: A module object representing the module, or an Error module if loading failed +:rtype: class bumblebee_status.module.Module +""" + + def load(module_name, config=core.config.Config([]), theme=None): error = None module_short, alias = (module_name.split(":") + [module_name])[0:2] @@ -35,6 +49,13 @@ def load(module_name, config=core.config.Config([]), theme=None): class Module(core.input.Object): + """Represents a module (single piece of functionality) of the bar + + :param config: Configuration to apply to the module (defaults to an empty configuration) + :param theme: Theme for this module, defaults to None, which means whatever is configured in "config" + :param widgets: A list of bumblebee_status.widget.Widget objects that the module is comprised of + """ + def __init__(self, config=core.config.Config([]), theme=None, widgets=[]): super().__init__() self.__config = config @@ -51,23 +72,55 @@ class Module(core.input.Object): for widget in self.__widgets: widget.module = self + """Override this to determine when to show this module + + :return: True if the module should be hidden, False otherwise + :rtype: boolean + """ + def hidden(self): return False + """Retrieve CLI/configuration parameters for this module. For example, if + the module is called "test" and the user specifies "-p test.x=123" on the + commandline, using self.parameter("x") retrieves the value 123. + + :param key: Name of the parameter to retrieve + :param default: Default value, if parameter is not set by user (defaults to None) + + :return: Parameter value, or default + :rtype: string + """ + def parameter(self, key, default=None): value = default for prefix in [self.name, self.module_name, self.alias]: value = self.__config.get("{}.{}".format(prefix, key), value) - # TODO retrieve from config file return value + """Set a parameter for this module + + :param key: Name of the parameter to set + :param value: New value of the parameter + """ + def set(self, key, value): self.__config.set("{}.{}".format(self.name, key), value) + """Override this method to define tasks that should be done during each + update interval (for instance, querying an API, calling a CLI tool to get new + date, etc. + """ + def update(self): pass + """Wrapper method that ensures that all exceptions thrown by the + update() method are caught and displayed in a bumblebee_status.module.Error + module + """ + def update_wrapper(self): try: self.update() @@ -77,17 +130,42 @@ class Module(core.input.Object): self.__widgets = [module.widget()] self.update = module.update + """Retrieves the list of widgets for this module + + :return: A list of widgets + :rtype: list of bumblebee_status.widget.Widgets + """ + def widgets(self): return self.__widgets + """Removes all widgets of this module""" + def clear_widgets(self): del self.__widgets[:] + """Adds a widget to the module + + :param full_text: Text or callable (method) that defines the text of the widget (defaults to "") + :param name: Name of the widget, defaults to None, which means autogenerate + + :return: The new widget + :rtype: bumblebee_status.widget.Widget + """ + def add_widget(self, full_text="", name=None): widget = core.widget.Widget(full_text=full_text, name=name, module=self) self.widgets().append(widget) return widget + """Convenience method to retrieve a named widget + + :param name: Name of widget to retrieve, defaults to None (in which case the first widget is returned) + + :return: The widget with the corresponding name, None if not found + :rtype: bumblebee_status.widget.Widget + """ + def widget(self, name=None): if not name: return self.widgets()[0] @@ -97,9 +175,27 @@ class Module(core.input.Object): return w return None + """Override this method to define states for the module + + :param widget: Widget for which state should be returned + + :return: a list of states for this widget + :rtype: list of strings + """ + def state(self, widget): return [] + """Convenience method that sets warning and critical state for numbers + + :param value: Current value to calculate state against + :param warn: Warning threshold + :parm crit: Critical threshold + + :return: None if value is below both thresholds, "critical", "warning" as appropriate otherwise + :rtype: string + """ + def threshold_state(self, value, warn, crit): if value > float(self.parameter("critical", crit)): return "critical" @@ -107,16 +203,49 @@ class Module(core.input.Object): return "warning" return None + def register_callbacks(self): + actions = [ + {"name": "left-click", "id": core.input.LEFT_MOUSE}, + {"name": "right-click", "id": core.input.RIGHT_MOUSE}, + {"name": "middle-click", "id": core.input.MIDDLE_MOUSE}, + {"name": "wheel-up", "id": core.input.WHEEL_UP}, + {"name": "wheel-down", "id": core.input.WHEEL_DOWN}, + ] + for action in actions: + if self.parameter(action["name"]): + core.input.register( + self, + action["id"], + self.parameter(action["name"]), + util.format.asbool( + self.parameter("{}-wait".format(action["name"]), False) + ), + ) + class Error(Module): + """Represents an "error" module + + :param module: The module name that produced the error + :param error: The error message to display + :param config: Configuration to apply to the module (defaults to an empty configuration) + :param theme: Theme for this module, defaults to None, which means whatever is configured in "config" + """ + def __init__(self, module, error, config=core.config.Config([]), theme=None): super().__init__(config, theme, core.widget.Widget(self.full_text)) self.__module = module self.__error = error + """Returns the error message + :param widget: the error widget to display + """ + def full_text(self, widget): return "{}: {}".format(self.__module, self.__error) + """Overriden state, always returns critical (it *is* an error, after all""" + def state(self, widget): return ["critical"] diff --git a/bumblebee_status/discover.py b/bumblebee_status/discover.py index 5c9e1bc..73e6c65 100644 --- a/bumblebee_status/discover.py +++ b/bumblebee_status/discover.py @@ -9,4 +9,19 @@ def discover(): sys.path.append(libdir) +def utility(name): + current_path = os.path.dirname(os.path.abspath(__file__)) + + for path in [ + os.path.join(current_path, "..", "bin"), + os.path.join( + current_path, "..", "..", "..", "..", "share", "bumblebee-status", "utility" + ), + "/usr/share/bumblebee-status/bin/", + ]: + if os.path.exists(os.path.abspath(os.path.join(path, name))): + return os.path.abspath(os.path.join(path, name)) + raise Exception("{} not found".format(name)) + + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/modules/contrib/arch-update.py b/bumblebee_status/modules/contrib/arch-update.py index 66287be..990cdbb 100644 --- a/bumblebee_status/modules/contrib/arch-update.py +++ b/bumblebee_status/modules/contrib/arch-update.py @@ -33,13 +33,18 @@ class Module(core.module.Module): return self.__packages == 0 and not self.__error def update(self): - try: - result = util.cli.execute("checkupdates") - self.__packages = len(result.split("\n")) - 1 - self.__error = False - except Exception as e: - logging.exception(e) + self.__error = False + code, result = util.cli.execute( + "checkupdates", ignore_errors=True, return_exitcode=True + ) + + if code == 0: + self.__packages = len(result.split("\n")) + elif code == 2: + self.__packages = 0 + else: self.__error = True + log.error("checkupdates exited with {}: {}".format(code, result)) def state(self, widget): if self.__error: diff --git a/bumblebee_status/modules/contrib/battery.py b/bumblebee_status/modules/contrib/battery.py index d11358e..512866d 100644 --- a/bumblebee_status/modules/contrib/battery.py +++ b/bumblebee_status/modules/contrib/battery.py @@ -115,7 +115,7 @@ class Module(core.module.Module): for battery in glob.glob("/sys/class/power_supply/BAT*") ] if len(self._batteries) == 0: - raise Exceptions("no batteries configured/found") + raise Exception("no batteries configured/found") core.input.register( self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics" ) diff --git a/bumblebee_status/modules/contrib/pacman.py b/bumblebee_status/modules/contrib/pacman.py index 8566112..b50ed53 100644 --- a/bumblebee_status/modules/contrib/pacman.py +++ b/bumblebee_status/modules/contrib/pacman.py @@ -22,15 +22,15 @@ import core.decorators import util.cli import util.format +from bumblebee_status.discover import utility + # list of repositories. # the last one should always be other repos = ["core", "extra", "community", "multilib", "testing", "other"] def get_pacman_info(widget, path): - cmd = "{}/../../bin/pacman-updates".format(path) - if not os.path.exists(cmd): - cmd = "/usr/share/bumblebee-status/bin/pacman-update" + cmd = utility("pacman-updates") result = util.cli.execute(cmd, ignore_errors=True) count = len(repos) * [0] diff --git a/bumblebee_status/modules/core/xrandr.py b/bumblebee_status/modules/core/xrandr.py index f3eabb6..04278ba 100644 --- a/bumblebee_status/modules/core/xrandr.py +++ b/bumblebee_status/modules/core/xrandr.py @@ -24,6 +24,8 @@ import core.module import core.input import core.decorators +from bumblebee_status.discover import utility + import util.cli import util.format @@ -36,8 +38,7 @@ except: class Module(core.module.Module): @core.decorators.every(seconds=5) # takes up to 5s to detect a new screen def __init__(self, config, theme): - widgets = [] - super().__init__(config, theme, widgets) + super().__init__(config, theme, []) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) self._needs_update = True @@ -85,10 +86,9 @@ class Module(core.module.Module): def _toggle(self, event): self._refresh(self, event) - path = os.path.dirname(os.path.abspath(__file__)) if util.format.asbool(self.parameter("overwrite_i3config", False)) == True: - toggle_cmd = "{}/../../bin/toggle-display.sh".format(path) + toggle_cmd = utility("toggle-display.sh") else: toggle_cmd = "xrandr" diff --git a/bumblebee_status/util/cli.py b/bumblebee_status/util/cli.py index fd54093..3e4face 100644 --- a/bumblebee_status/util/cli.py +++ b/bumblebee_status/util/cli.py @@ -4,7 +4,15 @@ import subprocess import logging -def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None): +def execute( + cmd, + wait=True, + ignore_errors=False, + include_stderr=False, + env=None, + return_exitcode=False, + shell=False, +): """Executes a commandline utility and returns its output :param cmd: the command (as string) to execute, returns the program's output @@ -12,13 +20,15 @@ def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None) :param ignore_errors: set to True to return a string when an exception is thrown, otherwise might throw, defaults to False :param include_stderr: set to True to include stderr output in the return value, defaults to False :param env: provide a dict here to specify a custom execution environment, defaults to None + :param return_exitcode: set to True to return a pair, where the first member is the exit code and the message the second, defaults to False + :param shell: set to True to run command in a separate shell, defaults to False :raises RuntimeError: the command either didn't exist or didn't exit cleanly, and ignore_errors was set to False - :return: output of cmd, or stderr, if ignore_errors is True and the command failed - :rtype: string + :return: output of cmd, or stderr, if ignore_errors is True and the command failed; or a tuple of exitcode and the previous, if return_exitcode is set to True + :rtype: string or tuple (if return_exitcode is set to True) """ - args = shlex.split(cmd) + args = cmd if shell else shlex.split(cmd) logging.debug(cmd) try: proc = subprocess.Popen( @@ -26,6 +36,7 @@ def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None) stdout=subprocess.PIPE, stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE, env=env, + shell=shell, ) except FileNotFoundError as e: raise RuntimeError("{} not found".format(cmd)) @@ -34,11 +45,14 @@ def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None) out, _ = proc.communicate() if proc.returncode != 0: err = "{} exited with code {}".format(cmd, proc.returncode) + logging.warning(err) if ignore_errors: - return err + return (proc.returncode, err) if return_exitcode else err raise RuntimeError(err) - return out.decode("utf-8") - return "" + res = out.decode("utf-8") + logging.debug(res) + return (proc.returncode, res) if return_exitcode else res + return (0, "") if return_exitcode else "" # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee_status/util/graph.py b/bumblebee_status/util/graph.py index b305477..fa927e2 100644 --- a/bumblebee_status/util/graph.py +++ b/bumblebee_status/util/graph.py @@ -2,22 +2,13 @@ MAX_PERCENTS = 100.0 class Bar(object): - """superclass""" - bars = None def __init__(self, value): - """ - Args: - - value (float): value between 0. and 100. meaning percents - """ self.value = value class HBar(Bar): - """horizontal bar (1 char)""" - bars = [ "\u2581", "\u2582", @@ -29,20 +20,20 @@ class HBar(Bar): "\u2588", ] - def __init__(self, value): - """ - Args: + """This class is a helper class used to draw horizontal bars - please use hbar directly - value (float): value between 0. and 100. meaning percents - """ + :param value: percentage value to draw (float, between 0 and 100) + """ + + def __init__(self, value): super(HBar, self).__init__(value) self.step = MAX_PERCENTS / len(HBar.bars) def get_char(self): - """ - Decide which char to draw + """Returns the character representing the current object's value - Return: str + :return: character representing the value passed during initialization + :rtype: string with one character """ for i in range(len(HBar.bars)): left = i * self.step @@ -53,13 +44,16 @@ class HBar(Bar): def hbar(value): - """wrapper function""" + """"Retrieves the horizontal bar character representing the input value + + :param value: percentage value to draw (float, between 0 and 100) + :return: character representing the value passed during initialization + :rtype: string with one character + """ return HBar(value).get_char() class VBar(Bar): - """vertical bar (can be more than 1 char)""" - bars = [ "\u258f", "\u258e", @@ -71,24 +65,24 @@ class VBar(Bar): "\u2588", ] + """This class is a helper class used to draw vertical bars - please use vbar directly + + :param value: percentage value to draw (float, between 0 and 100) + :param width: maximum width of the bar in characters + """ + def __init__(self, value, width=1): - """ - Args: - - value (float): value between 0. and 100. meaning percents - - width (int): width - """ super(VBar, self).__init__(value) self.step = MAX_PERCENTS / (len(VBar.bars) * width) self.width = width - def get_chars(self): - """ - Decide which char to draw + """Returns the characters representing the current object's value - Return: str - """ + :return: characters representing the value passed during initialization + :rtype: string + """ + + def get_chars(self): if self.value == 100: return self.bars[-1] * self.width if self.width == 1: @@ -111,16 +105,18 @@ class VBar(Bar): def vbar(value, width): - """wrapper function""" + """Returns the characters representing the current object's value + + :param value: percentage value to draw (float, between 0 and 100) + :param width: maximum width of the bar in characters + + :return: characters representing the value passed during initialization + :rtype: string + """ return VBar(value, width).get_chars() class BrailleGraph(object): - """ - graph using Braille chars - scaled to passed values - """ - chars = { (0, 0): " ", (1, 0): "\u2840", @@ -149,12 +145,12 @@ class BrailleGraph(object): (4, 4): "\u28ff", } - def __init__(self, values): - """ - Args: + """This class is a helper class used to draw braille graphs - please use braille directly - values (list): list of values - """ + :param values: values to draw + """ + + def __init__(self, values): self.values = values # length of values list must be even # because one Braille char displays two values @@ -165,15 +161,6 @@ class BrailleGraph(object): @staticmethod def get_height(value, unit): - """ - Compute height of a value relative to unit - - Args: - - value (number): value - - unit (number): unit - """ if value < unit / 10.0: return 0 elif value <= unit: @@ -186,11 +173,6 @@ class BrailleGraph(object): return 4 def get_steps(self): - """ - Convert the list of values to a list of steps - - Return: list - """ maxval = max(self.values) unit = maxval / 4.0 if unit == 0: @@ -201,11 +183,6 @@ class BrailleGraph(object): return stepslist def get_chars(self): - """ - Decide which chars to draw - - Return: str - """ chars = [] for part in self.parts: chars.append(BrailleGraph.chars[part]) @@ -213,7 +190,6 @@ class BrailleGraph(object): def braille(values): - """wrapper function""" return BrailleGraph(values).get_chars() diff --git a/bumblebee_status/util/popup.py b/bumblebee_status/util/popup.py index 23f3587..dee67f6 100644 --- a/bumblebee_status/util/popup.py +++ b/bumblebee_status/util/popup.py @@ -2,51 +2,82 @@ import logging -try: - import Tkinter as tk -except ImportError: - # python 3 - import tkinter as tk +import tkinter as tk import functools class menu(object): + """Draws a hierarchical popup menu + + :param parent: If given, this menu is a leave of the "parent" menu + :param leave: If set to True, close this menu when mouse leaves the area (defaults to True) + """ + def __init__(self, parent=None, leave=True): if not parent: self._root = tk.Tk() self._root.withdraw() self._menu = tk.Menu(self._root, tearoff=0) - self._menu.bind("", self._on_focus_out) + self._menu.bind("", self.__on_focus_out) else: self._root = parent.root() self._root.withdraw() self._menu = tk.Menu(self._root, tearoff=0) - self._menu.bind("", self._on_focus_out) + self._menu.bind("", self.__on_focus_out) if leave: - self._menu.bind("", self._on_focus_out) + self._menu.bind("", self.__on_focus_out) + + """Returns the root node of this menu + + :return: root node + """ def root(self): return self._root + """Returns the menu + + :return: menu + """ + def menu(self): return self._menu - def _on_focus_out(self, event=None): + def __on_focus_out(self, event=None): self._root.destroy() - def _on_click(self, callback): + def __on_click(self, callback): self._root.destroy() callback() + """Adds a cascading submenu to the current menu + + :param menuitem: label to display for the submenu + :param submenu: submenu to show + """ + def add_cascade(self, menuitem, submenu): self._menu.add_cascade(label=menuitem, menu=submenu.menu()) + """Adds an item to the current menu + + :param menuitem: label to display for the entry + :param callback: method to invoke on click + """ + def add_menuitem(self, menuitem, callback): self._menu.add_command( - label=menuitem, command=functools.partial(self._on_click, callback) + label=menuitem, command=functools.partial(self.__on_click, callback) ) + """Shows this menu + + :param event: i3wm event that triggered the menu (dict that contains "x" and "y" fields) + :param offset_x: x-axis offset from mouse position for the menu (defaults to 0) + :param offset_y: y-axis offset from mouse position for the menu (defaults to 0) + """ + def show(self, event, offset_x=0, offset_y=0): try: self._menu.tk_popup(event["x"] + offset_x, event["y"] + offset_y) diff --git a/docs/api.rst b/docs/api.rst index a089602..7c22a2b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,5 +4,4 @@ API Reference .. toctree:: :maxdepth: 4 - src/core - src/util + src/bumblebee_status diff --git a/docs/other/NOTES.md b/docs/other/NOTES.md index 1b4e699..4ff62c4 100644 --- a/docs/other/NOTES.md +++ b/docs/other/NOTES.md @@ -9,11 +9,13 @@ - use __ for private ## Improvements +- app launcher (list of apps, themeable) ## TODO - themes: use colors to improve theme readability - convert some stuff to simple attributes to reduce LOCs - use widget index for bumblebee-ctl as alternative (??) +- use pytest? # documentation Add info about error widget and events for error logging diff --git a/docs/src/bumblebee_status.core.rst b/docs/src/bumblebee_status.core.rst new file mode 100644 index 0000000..14b545c --- /dev/null +++ b/docs/src/bumblebee_status.core.rst @@ -0,0 +1,78 @@ +bumblebee\_status.core package +============================== + +Submodules +---------- + +bumblebee\_status.core.config module +------------------------------------ + +.. automodule:: bumblebee_status.core.config + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.decorators module +---------------------------------------- + +.. automodule:: bumblebee_status.core.decorators + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.event module +----------------------------------- + +.. automodule:: bumblebee_status.core.event + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.input module +----------------------------------- + +.. automodule:: bumblebee_status.core.input + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.module module +------------------------------------ + +.. automodule:: bumblebee_status.core.module + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.output module +------------------------------------ + +.. automodule:: bumblebee_status.core.output + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.theme module +----------------------------------- + +.. automodule:: bumblebee_status.core.theme + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.core.widget module +------------------------------------ + +.. automodule:: bumblebee_status.core.widget + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: bumblebee_status.core + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/src/bumblebee_status.rst b/docs/src/bumblebee_status.rst new file mode 100644 index 0000000..1519661 --- /dev/null +++ b/docs/src/bumblebee_status.rst @@ -0,0 +1,31 @@ +bumblebee\_status package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + bumblebee_status.core + bumblebee_status.util + +Submodules +---------- + +bumblebee\_status.discover module +--------------------------------- + +.. automodule:: bumblebee_status.discover + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: bumblebee_status + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/src/bumblebee_status.util.rst b/docs/src/bumblebee_status.util.rst new file mode 100644 index 0000000..98cf474 --- /dev/null +++ b/docs/src/bumblebee_status.util.rst @@ -0,0 +1,70 @@ +bumblebee\_status.util package +============================== + +Submodules +---------- + +bumblebee\_status.util.algorithm module +--------------------------------------- + +.. automodule:: bumblebee_status.util.algorithm + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.cli module +--------------------------------- + +.. automodule:: bumblebee_status.util.cli + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.format module +------------------------------------ + +.. automodule:: bumblebee_status.util.format + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.graph module +----------------------------------- + +.. automodule:: bumblebee_status.util.graph + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.location module +-------------------------------------- + +.. automodule:: bumblebee_status.util.location + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.popup module +----------------------------------- + +.. automodule:: bumblebee_status.util.popup + :members: + :undoc-members: + :show-inheritance: + +bumblebee\_status.util.store module +----------------------------------- + +.. automodule:: bumblebee_status.util.store + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: bumblebee_status.util + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/src/core.rst b/docs/src/core.rst deleted file mode 100644 index e1b2eed..0000000 --- a/docs/src/core.rst +++ /dev/null @@ -1,78 +0,0 @@ -core package -============ - -Submodules ----------- - -core.config module ------------------- - -.. automodule:: core.config - :members: - :undoc-members: - :show-inheritance: - -core.decorators module ----------------------- - -.. automodule:: core.decorators - :members: - :undoc-members: - :show-inheritance: - -core.event module ------------------ - -.. automodule:: core.event - :members: - :undoc-members: - :show-inheritance: - -core.input module ------------------ - -.. automodule:: core.input - :members: - :undoc-members: - :show-inheritance: - -core.module module ------------------- - -.. automodule:: core.module - :members: - :undoc-members: - :show-inheritance: - -core.output module ------------------- - -.. automodule:: core.output - :members: - :undoc-members: - :show-inheritance: - -core.theme module ------------------ - -.. automodule:: core.theme - :members: - :undoc-members: - :show-inheritance: - -core.widget module ------------------- - -.. automodule:: core.widget - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: core - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/src/util.rst b/docs/src/util.rst deleted file mode 100644 index e127fca..0000000 --- a/docs/src/util.rst +++ /dev/null @@ -1,70 +0,0 @@ -util package -============ - -Submodules ----------- - -util.algorithm module ---------------------- - -.. automodule:: util.algorithm - :members: - :undoc-members: - :show-inheritance: - -util.cli module ---------------- - -.. automodule:: util.cli - :members: - :undoc-members: - :show-inheritance: - -util.format module ------------------- - -.. automodule:: util.format - :members: - :undoc-members: - :show-inheritance: - -util.graph module ------------------ - -.. automodule:: util.graph - :members: - :undoc-members: - :show-inheritance: - -util.location module --------------------- - -.. automodule:: util.location - :members: - :undoc-members: - :show-inheritance: - -util.popup module ------------------ - -.. automodule:: util.popup - :members: - :undoc-members: - :show-inheritance: - -util.store module ------------------ - -.. automodule:: util.store - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: util - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/themes.rst b/docs/themes.rst index fa0493e..759ea86 100644 --- a/docs/themes.rst +++ b/docs/themes.rst @@ -88,6 +88,11 @@ List of available themes Nord Powerline (-t nord-powerline) (contributed by `uselessthird `__) +.. figure:: ../screenshots/themes/night-powerline.png + :alt: Night Powerline + + Night Powerline (-t night-powerline) (contributed by `LtPeriwinkle `__) + .. figure:: ../screenshots/themes/default.png :alt: Default diff --git a/screenshots/themes/night-powerline.png b/screenshots/themes/night-powerline.png new file mode 100644 index 0000000..dca9d65 Binary files /dev/null and b/screenshots/themes/night-powerline.png differ diff --git a/setup.cfg b/setup.cfg index 5caf4e6..9352f13 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,9 +39,6 @@ packages = find: scripts = ./bumblebee-status ./bumblebee-ctl - ./bin/load-i3-bars.sh - ./bin/pacman-updates - ./bin/toggle-display.sh [versioneer] VCS = git diff --git a/setup.py b/setup.py index 79ccd29..f9f2ee4 100755 --- a/setup.py +++ b/setup.py @@ -53,8 +53,10 @@ setup( version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), zip_safe=False, + test_suite="tests", data_files=[ ("share/bumblebee-status/themes", glob.glob("themes/*.json")), ("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")), + ("share/bumblebee-status/utility", glob.glob("bin/*")), ], ) diff --git a/tests/core/test_config.py b/tests/core/test_config.py index 27931ff..e468f3a 100644 --- a/tests/core/test_config.py +++ b/tests/core/test_config.py @@ -52,8 +52,8 @@ class config(unittest.TestCase): def test_logfile(self): cfg = core.config.Config(["-f", "my-custom-logfile"]) - self.assertEquals(None, self.defaultConfig.logfile()) - self.assertEquals("my-custom-logfile", cfg.logfile()) + self.assertEqual(None, self.defaultConfig.logfile()) + self.assertEqual("my-custom-logfile", cfg.logfile()) def test_all_modules(self): modules = core.config.all_modules() diff --git a/tests/core/test_decorators.py b/tests/core/test_decorators.py index 9ad7986..ab924da 100644 --- a/tests/core/test_decorators.py +++ b/tests/core/test_decorators.py @@ -7,6 +7,7 @@ import core.config class TestModule(core.module.Module): + @core.decorators.never def __init__(self, config=None, theme=None): config = core.config.Config([]) super().__init__(config, theme, core.widget.Widget(self.get)) @@ -24,6 +25,10 @@ class config(unittest.TestCase): self.width = 10 self.module.set("scrolling.width", self.width) + def test_never(self): + self.module = TestModule() + self.assertEqual("never", self.module.parameter("interval")) + def test_no_text(self): self.assertEqual("", self.module.text) self.assertEqual("", self.module.get(self.widget)) @@ -70,5 +75,25 @@ class config(unittest.TestCase): self.module.text = "wxyz" self.assertEqual("wx", self.module.get(self.widget)) + def test_minimum_changed_data(self): + self.module.text = "this is a sample song (0:00)" + self.module.set("scrolling.width", 10) + self.assertEqual(self.module.text[0:10], self.module.get(self.widget)) + self.module.text = "this is a sample song (0:01)" + self.assertEqual(self.module.text[1:11], self.module.get(self.widget)) + self.module.text = "this is a sample song (0:12)" + self.assertEqual(self.module.text[2:12], self.module.get(self.widget)) + self.module.text = "this is a different song (0:12)" + self.assertEqual(self.module.text[0:10], self.module.get(self.widget)) + + def test_n_plus_one(self): + self.module.text = "10 letters" + self.module.set("scrolling.width", 9) + self.assertEqual(self.module.text[0:9], self.module.get(self.widget)) + self.assertEqual(self.module.text[1:10], self.module.get(self.widget)) + self.assertEqual(self.module.text[0:9], self.module.get(self.widget)) + self.assertEqual(self.module.text[1:10], self.module.get(self.widget)) + self.assertEqual(self.module.text[0:9], self.module.get(self.widget)) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/tests/core/test_input.py b/tests/core/test_input.py index 667184a..ba5641e 100644 --- a/tests/core/test_input.py +++ b/tests/core/test_input.py @@ -70,7 +70,9 @@ class config(unittest.TestCase): self.inputObject, self.someEvent["button"], self.someCommand ) core.input.trigger(self.someEvent) - cli.execute.assert_called_once_with(self.someCommand, wait=False) + cli.execute.assert_called_once_with( + self.someCommand, wait=False, shell=True + ) def test_non_existent_callback(self): with unittest.mock.patch("core.input.util.cli") as cli: diff --git a/tests/core/test_module.py b/tests/core/test_module.py index 71f59ad..7628aac 100644 --- a/tests/core/test_module.py +++ b/tests/core/test_module.py @@ -6,6 +6,7 @@ import shlex import core.module import core.widget import core.config +import core.input class TestModule(core.module.Module): @@ -137,5 +138,21 @@ class module(unittest.TestCase): self.assertEqual(None, module.threshold_state(80, 80, 100)) self.assertEqual(None, module.threshold_state(10, 80, 100)) + def test_configured_callbacks(self): + cfg = core.config.Config([]) + module = TestModule(config=cfg, widgets=[self.someWidget, self.anotherWidget]) + + cmd = "sample-tool arg1 arg2 arg3" + module.set("left-click", cmd) + module.register_callbacks() + + with unittest.mock.patch("core.input.util.cli") as cli: + cli.execute.return_value = "" + core.input.trigger( + {"button": core.input.LEFT_MOUSE, "instance": module.id,} + ) + + cli.execute.assert_called_once_with(cmd, wait=False, shell=True) + # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/night-powerline.json b/themes/night-powerline.json new file mode 100644 index 0000000..01929b9 --- /dev/null +++ b/themes/night-powerline.json @@ -0,0 +1,63 @@ +{ + "icons": [ "awesome-fonts" ], + "defaults": { + "separator-block-width": 0, + "warning": { + "fg": "#afafaf", + "bg": "#4d401d" + }, + "critical": { + "fg": "#afafaf", + "bg": "#6e0b0a" + } + }, + "cycle": [ + { "fg": "#afafaf", "bg": "#0f0f0f" }, + { "fg": "#afafaf", "bg": "#1f1f1f" }, + { "fg": "#afafaf", "bg": "#2b2b2b" }, + { "fg": "#afafaf", "bg": "#1e1e1e" } + ], + "dnf": { + "good": { + "fg": "#afafaf", + "bg": "#26362d" + } + }, + "apt": { + "good": { + "fg": "#afafaf", + "bg": "#26362d" + } + }, + "pacman": { + "good": { + "fg": "#b2b2b2", + "bg": "#26362d" + } + }, + "battery": { + "charged": { + "fg": "#afafaf", + "bg": "#26362d" + }, + "AC": { + "fg": "#afafaf", + "bg": "#26362d" + } + }, + "pomodoro": { + "paused": { + "fg": "#afafaf", + "bg": "#b58900" + }, + "work": { + "fg": "#1d2021", + "bg": "#b8bb26" + }, + "break": { + "fg": "#afafaf", + "bg": "#26362d" + } + } + +}