Merge branch 'development' into arandr

This commit is contained in:
Zero Rust 2020-05-18 06:58:07 -04:00
commit 2fcd277cf9
34 changed files with 713 additions and 270 deletions

View file

@ -13,4 +13,3 @@ ratings:
- "**.py" - "**.py"
exclude_paths: exclude_paths:
- tests/ - tests/
- thirdparty/

27
.github/workflows/pythonpublish.yml vendored Normal file
View file

@ -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/*

3
.gitignore vendored
View file

@ -94,3 +94,6 @@ ENV/
# Visual studio project files # Visual studio project files
.vscode/ .vscode/
# mypy cache
.mypy_cache

View file

@ -8,17 +8,9 @@ python:
- "3.8" - "3.8"
before_install: before_install:
- sudo apt-get -qq update - sudo apt-get -qq update
- sudo apt-get install -y task libdbus-1-dev
install: install:
- pip install i3ipc - pip install coverage
- pip install psutil
- pip install netifaces
- pip install -U coverage==4.3
- pip install codeclimate-test-reporter - pip install codeclimate-test-reporter
- pip install taskw
- pip install pytz
- pip install tzlocal
- pip install dbus-python
- pip install coverage - pip install coverage
script: script:
- coverage run -m unittest discover -v - coverage run -m unittest discover -v

View file

@ -10,6 +10,7 @@ import logging
import threading import threading
import bumblebee_status.discover import bumblebee_status.discover
bumblebee_status.discover.discover() bumblebee_status.discover.discover()
import core.config import core.config
@ -101,6 +102,7 @@ def main():
for module in config.modules(): for module in config.modules():
modules.append(core.module.load(module, config, theme)) modules.append(core.module.load(module, config, theme))
modules[-1].register_callbacks()
if config.reverse(): if config.reverse():
modules.reverse() modules.reverse()

View file

@ -0,0 +1,3 @@
import bumblebee_status.discover
bumblebee_status.discover.discover()

View file

@ -27,7 +27,11 @@ THEME_HELP = "Specify the theme to use for drawing modules"
def all_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 = {} result = {}
for path in [modules.core.__file__, modules.contrib.__file__]: for path in [modules.core.__file__, modules.contrib.__file__]:
@ -127,6 +131,11 @@ class print_usage(argparse.Action):
class Config(util.store.Store): 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): def __init__(self, args):
super(Config, self).__init__() super(Config, self).__init__()
@ -202,6 +211,11 @@ class Config(util.store.Store):
key, value = param.split("=", 1) key, value = param.split("=", 1)
self.set(key, value) 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): def load_config(self, filename):
if os.path.exists(filename): if os.path.exists(filename):
log.info("loading {}".format(filename)) log.info("loading {}".format(filename))
@ -212,27 +226,75 @@ class Config(util.store.Store):
for key, value in tmp.items("module-parameters"): for key, value in tmp.items("module-parameters"):
self.set(key, value) self.set(key, value)
"""Returns a list of configured modules
:return: list of configured (active) modules
:rtype: list of strings
"""
def modules(self): def modules(self):
return [item for sub in self.__args.modules for item in sub] 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): def interval(self, default=1):
return util.format.seconds(self.get("interval", default)) 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): def debug(self):
return self.__args.debug 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): def reverse(self):
return self.__args.right_to_left return self.__args.right_to_left
"""Returns the logfile location
:return: location where the logfile should be written
:rtype: string
"""
def logfile(self): def logfile(self):
return self.__args.logfile return self.__args.logfile
"""Returns the configured theme name
:return: name of the configured theme
:rtype: string
"""
def theme(self): def theme(self):
return self.__args.theme return self.__args.theme
"""Returns the configured iconset name
:return: name of the configured iconset
:rtype: string
"""
def iconset(self): def iconset(self):
return self.__args.iconset 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): def autohide(self, name):
return name in self.__args.autohide return name in self.__args.autohide

View file

@ -1,5 +1,19 @@
import difflib
import logging
import util.format 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 never(init):
def call_init(obj, *args, **kwargs): def call_init(obj, *args, **kwargs):
@ -10,6 +24,16 @@ def never(init):
return call_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 every(hours=0, minutes=0, seconds=0):
def decorator_init(init): def decorator_init(init):
def call_init(obj, *args, **kwargs): def call_init(obj, *args, **kwargs):
@ -22,13 +46,30 @@ def every(hours=0, minutes=0, seconds=0):
return decorator_init 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 scrollable(func):
def wrapper(module, widget): def wrapper(module, widget):
text = func(module, widget) text = func(module, widget)
if not text: if not text:
return 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.start", 0)
widget.set("scrolling.direction", "right") widget.set("scrolling.direction", "right")
widget.set("__content__", text) widget.set("__content__", text)
@ -45,9 +86,10 @@ def scrollable(func):
direction = widget.get("scrolling.direction", "right") direction = widget.get("scrolling.direction", "right")
if direction == "left": 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") widget.set("scrolling.direction", "right")
else:
scroll_speed = -scroll_speed
next_start = start + scroll_speed next_start = start + scroll_speed
if next_start + width > len(text): if next_start + width > len(text):

View file

@ -36,20 +36,20 @@ def __event_id(obj_id, button):
return "{}::{}".format(obj_id, button_name(button)) return "{}::{}".format(obj_id, button_name(button))
def __execute(cmd): def __execute(cmd, wait=False):
try: try:
util.cli.execute(cmd, wait=False) util.cli.execute(cmd, wait=wait, shell=True)
except Exception as e: except Exception as e:
logging.error("failed to invoke callback: {}".format(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) event_id = __event_id(obj.id if obj is not None else "", button)
logging.debug("registering callback {}".format(event_id)) logging.debug("registering callback {}".format(event_id))
if callable(cmd): if callable(cmd):
core.event.register(event_id, cmd) core.event.register(event_id, cmd)
else: else:
core.event.register(event_id, lambda _: __execute(cmd)) core.event.register(event_id, lambda _: __execute(cmd, wait))
def trigger(event): def trigger(event):

View file

@ -2,10 +2,13 @@ import os
import importlib import importlib
import logging import logging
import core.config
import core.input import core.input
import core.widget import core.widget
import core.decorators import core.decorators
import util.format
try: try:
error = ModuleNotFoundError("") error = ModuleNotFoundError("")
except Exception as e: except Exception as e:
@ -14,6 +17,17 @@ except Exception as e:
log = logging.getLogger(__name__) 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): def load(module_name, config=core.config.Config([]), theme=None):
error = None error = None
module_short, alias = (module_name.split(":") + [module_name])[0:2] 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): 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=[]): def __init__(self, config=core.config.Config([]), theme=None, widgets=[]):
super().__init__() super().__init__()
self.__config = config self.__config = config
@ -51,23 +72,55 @@ class Module(core.input.Object):
for widget in self.__widgets: for widget in self.__widgets:
widget.module = self 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): def hidden(self):
return False 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): def parameter(self, key, default=None):
value = default value = default
for prefix in [self.name, self.module_name, self.alias]: for prefix in [self.name, self.module_name, self.alias]:
value = self.__config.get("{}.{}".format(prefix, key), value) value = self.__config.get("{}.{}".format(prefix, key), value)
# TODO retrieve from config file
return value 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): def set(self, key, value):
self.__config.set("{}.{}".format(self.name, 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): def update(self):
pass 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): def update_wrapper(self):
try: try:
self.update() self.update()
@ -77,17 +130,42 @@ class Module(core.input.Object):
self.__widgets = [module.widget()] self.__widgets = [module.widget()]
self.update = module.update 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): def widgets(self):
return self.__widgets return self.__widgets
"""Removes all widgets of this module"""
def clear_widgets(self): def clear_widgets(self):
del self.__widgets[:] 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): def add_widget(self, full_text="", name=None):
widget = core.widget.Widget(full_text=full_text, name=name, module=self) widget = core.widget.Widget(full_text=full_text, name=name, module=self)
self.widgets().append(widget) self.widgets().append(widget)
return 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): def widget(self, name=None):
if not name: if not name:
return self.widgets()[0] return self.widgets()[0]
@ -97,9 +175,27 @@ class Module(core.input.Object):
return w return w
return None 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): def state(self, widget):
return [] 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): def threshold_state(self, value, warn, crit):
if value > float(self.parameter("critical", crit)): if value > float(self.parameter("critical", crit)):
return "critical" return "critical"
@ -107,16 +203,49 @@ class Module(core.input.Object):
return "warning" return "warning"
return None 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): 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): def __init__(self, module, error, config=core.config.Config([]), theme=None):
super().__init__(config, theme, core.widget.Widget(self.full_text)) super().__init__(config, theme, core.widget.Widget(self.full_text))
self.__module = module self.__module = module
self.__error = error self.__error = error
"""Returns the error message
:param widget: the error widget to display
"""
def full_text(self, widget): def full_text(self, widget):
return "{}: {}".format(self.__module, self.__error) return "{}: {}".format(self.__module, self.__error)
"""Overriden state, always returns critical (it *is* an error, after all"""
def state(self, widget): def state(self, widget):
return ["critical"] return ["critical"]

View file

@ -9,4 +9,19 @@ def discover():
sys.path.append(libdir) 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -33,13 +33,18 @@ class Module(core.module.Module):
return self.__packages == 0 and not self.__error return self.__packages == 0 and not self.__error
def update(self): def update(self):
try:
result = util.cli.execute("checkupdates")
self.__packages = len(result.split("\n")) - 1
self.__error = False self.__error = False
except Exception as e: code, result = util.cli.execute(
logging.exception(e) "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 self.__error = True
log.error("checkupdates exited with {}: {}".format(code, result))
def state(self, widget): def state(self, widget):
if self.__error: if self.__error:

View file

@ -115,7 +115,7 @@ class Module(core.module.Module):
for battery in glob.glob("/sys/class/power_supply/BAT*") for battery in glob.glob("/sys/class/power_supply/BAT*")
] ]
if len(self._batteries) == 0: if len(self._batteries) == 0:
raise Exceptions("no batteries configured/found") raise Exception("no batteries configured/found")
core.input.register( core.input.register(
self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics" self, button=core.input.LEFT_MOUSE, cmd="gnome-power-statistics"
) )

View file

@ -22,15 +22,15 @@ import core.decorators
import util.cli import util.cli
import util.format import util.format
from bumblebee_status.discover import utility
# list of repositories. # list of repositories.
# the last one should always be other # the last one should always be other
repos = ["core", "extra", "community", "multilib", "testing", "other"] repos = ["core", "extra", "community", "multilib", "testing", "other"]
def get_pacman_info(widget, path): def get_pacman_info(widget, path):
cmd = "{}/../../bin/pacman-updates".format(path) cmd = utility("pacman-updates")
if not os.path.exists(cmd):
cmd = "/usr/share/bumblebee-status/bin/pacman-update"
result = util.cli.execute(cmd, ignore_errors=True) result = util.cli.execute(cmd, ignore_errors=True)
count = len(repos) * [0] count = len(repos) * [0]

View file

@ -24,6 +24,8 @@ import core.module
import core.input import core.input
import core.decorators import core.decorators
from bumblebee_status.discover import utility
import util.cli import util.cli
import util.format import util.format
@ -36,8 +38,7 @@ except:
class Module(core.module.Module): class Module(core.module.Module):
@core.decorators.every(seconds=5) # takes up to 5s to detect a new screen @core.decorators.every(seconds=5) # takes up to 5s to detect a new screen
def __init__(self, config, theme): def __init__(self, config, theme):
widgets = [] super().__init__(config, theme, [])
super().__init__(config, theme, widgets)
self._autoupdate = util.format.asbool(self.parameter("autoupdate", True)) self._autoupdate = util.format.asbool(self.parameter("autoupdate", True))
self._needs_update = True self._needs_update = True
@ -85,10 +86,9 @@ class Module(core.module.Module):
def _toggle(self, event): def _toggle(self, event):
self._refresh(self, event) self._refresh(self, event)
path = os.path.dirname(os.path.abspath(__file__))
if util.format.asbool(self.parameter("overwrite_i3config", False)) == True: 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: else:
toggle_cmd = "xrandr" toggle_cmd = "xrandr"

View file

@ -4,7 +4,15 @@ import subprocess
import logging 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 """Executes a commandline utility and returns its output
:param cmd: the command (as string) to execute, returns the program's 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 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 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 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 :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 :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 :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) logging.debug(cmd)
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
@ -26,6 +36,7 @@ def execute(cmd, wait=True, ignore_errors=False, include_stderr=False, env=None)
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE, stderr=subprocess.STDOUT if include_stderr else subprocess.PIPE,
env=env, env=env,
shell=shell,
) )
except FileNotFoundError as e: except FileNotFoundError as e:
raise RuntimeError("{} not found".format(cmd)) 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() out, _ = proc.communicate()
if proc.returncode != 0: if proc.returncode != 0:
err = "{} exited with code {}".format(cmd, proc.returncode) err = "{} exited with code {}".format(cmd, proc.returncode)
logging.warning(err)
if ignore_errors: if ignore_errors:
return err return (proc.returncode, err) if return_exitcode else err
raise RuntimeError(err) raise RuntimeError(err)
return out.decode("utf-8") res = out.decode("utf-8")
return "" 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -2,22 +2,13 @@ MAX_PERCENTS = 100.0
class Bar(object): class Bar(object):
"""superclass"""
bars = None bars = None
def __init__(self, value): def __init__(self, value):
"""
Args:
value (float): value between 0. and 100. meaning percents
"""
self.value = value self.value = value
class HBar(Bar): class HBar(Bar):
"""horizontal bar (1 char)"""
bars = [ bars = [
"\u2581", "\u2581",
"\u2582", "\u2582",
@ -29,20 +20,20 @@ class HBar(Bar):
"\u2588", "\u2588",
] ]
def __init__(self, value): """This class is a helper class used to draw horizontal bars - please use hbar directly
"""
Args:
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) super(HBar, self).__init__(value)
self.step = MAX_PERCENTS / len(HBar.bars) self.step = MAX_PERCENTS / len(HBar.bars)
def get_char(self): def get_char(self):
""" """Returns the character representing the current object's value
Decide which char to draw
Return: str :return: character representing the value passed during initialization
:rtype: string with one character
""" """
for i in range(len(HBar.bars)): for i in range(len(HBar.bars)):
left = i * self.step left = i * self.step
@ -53,13 +44,16 @@ class HBar(Bar):
def hbar(value): 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() return HBar(value).get_char()
class VBar(Bar): class VBar(Bar):
"""vertical bar (can be more than 1 char)"""
bars = [ bars = [
"\u258f", "\u258f",
"\u258e", "\u258e",
@ -71,24 +65,24 @@ class VBar(Bar):
"\u2588", "\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): def __init__(self, value, width=1):
"""
Args:
value (float): value between 0. and 100. meaning percents
width (int): width
"""
super(VBar, self).__init__(value) super(VBar, self).__init__(value)
self.step = MAX_PERCENTS / (len(VBar.bars) * width) self.step = MAX_PERCENTS / (len(VBar.bars) * width)
self.width = width self.width = width
def get_chars(self): """Returns the characters representing the current object's value
"""
Decide which char to draw
Return: str :return: characters representing the value passed during initialization
:rtype: string
""" """
def get_chars(self):
if self.value == 100: if self.value == 100:
return self.bars[-1] * self.width return self.bars[-1] * self.width
if self.width == 1: if self.width == 1:
@ -111,16 +105,18 @@ class VBar(Bar):
def vbar(value, width): 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() return VBar(value, width).get_chars()
class BrailleGraph(object): class BrailleGraph(object):
"""
graph using Braille chars
scaled to passed values
"""
chars = { chars = {
(0, 0): " ", (0, 0): " ",
(1, 0): "\u2840", (1, 0): "\u2840",
@ -149,12 +145,12 @@ class BrailleGraph(object):
(4, 4): "\u28ff", (4, 4): "\u28ff",
} }
def __init__(self, values): """This class is a helper class used to draw braille graphs - please use braille directly
"""
Args:
values (list): list of values :param values: values to draw
""" """
def __init__(self, values):
self.values = values self.values = values
# length of values list must be even # length of values list must be even
# because one Braille char displays two values # because one Braille char displays two values
@ -165,15 +161,6 @@ class BrailleGraph(object):
@staticmethod @staticmethod
def get_height(value, unit): 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: if value < unit / 10.0:
return 0 return 0
elif value <= unit: elif value <= unit:
@ -186,11 +173,6 @@ class BrailleGraph(object):
return 4 return 4
def get_steps(self): def get_steps(self):
"""
Convert the list of values to a list of steps
Return: list
"""
maxval = max(self.values) maxval = max(self.values)
unit = maxval / 4.0 unit = maxval / 4.0
if unit == 0: if unit == 0:
@ -201,11 +183,6 @@ class BrailleGraph(object):
return stepslist return stepslist
def get_chars(self): def get_chars(self):
"""
Decide which chars to draw
Return: str
"""
chars = [] chars = []
for part in self.parts: for part in self.parts:
chars.append(BrailleGraph.chars[part]) chars.append(BrailleGraph.chars[part])
@ -213,7 +190,6 @@ class BrailleGraph(object):
def braille(values): def braille(values):
"""wrapper function"""
return BrailleGraph(values).get_chars() return BrailleGraph(values).get_chars()

View file

@ -2,51 +2,82 @@
import logging import logging
try:
import Tkinter as tk
except ImportError:
# python 3
import tkinter as tk import tkinter as tk
import functools import functools
class menu(object): 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): def __init__(self, parent=None, leave=True):
if not parent: if not parent:
self._root = tk.Tk() self._root = tk.Tk()
self._root.withdraw() self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0) self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out) self._menu.bind("<FocusOut>", self.__on_focus_out)
else: else:
self._root = parent.root() self._root = parent.root()
self._root.withdraw() self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0) self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out) self._menu.bind("<FocusOut>", self.__on_focus_out)
if leave: if leave:
self._menu.bind("<Leave>", self._on_focus_out) self._menu.bind("<Leave>", self.__on_focus_out)
"""Returns the root node of this menu
:return: root node
"""
def root(self): def root(self):
return self._root return self._root
"""Returns the menu
:return: menu
"""
def menu(self): def menu(self):
return self._menu return self._menu
def _on_focus_out(self, event=None): def __on_focus_out(self, event=None):
self._root.destroy() self._root.destroy()
def _on_click(self, callback): def __on_click(self, callback):
self._root.destroy() self._root.destroy()
callback() 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): def add_cascade(self, menuitem, submenu):
self._menu.add_cascade(label=menuitem, menu=submenu.menu()) 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): def add_menuitem(self, menuitem, callback):
self._menu.add_command( 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): def show(self, event, offset_x=0, offset_y=0):
try: try:
self._menu.tk_popup(event["x"] + offset_x, event["y"] + offset_y) self._menu.tk_popup(event["x"] + offset_x, event["y"] + offset_y)

View file

@ -4,5 +4,4 @@ API Reference
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 4
src/core src/bumblebee_status
src/util

View file

@ -9,11 +9,13 @@
- use __ for private - use __ for private
## Improvements ## Improvements
- app launcher (list of apps, themeable)
## TODO ## TODO
- themes: use colors to improve theme readability - themes: use colors to improve theme readability
- convert some stuff to simple attributes to reduce LOCs - convert some stuff to simple attributes to reduce LOCs
- use widget index for bumblebee-ctl as alternative (??) - use widget index for bumblebee-ctl as alternative (??)
- use pytest?
# documentation # documentation
Add info about error widget and events for error logging Add info about error widget and events for error logging

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -88,6 +88,11 @@ List of available themes
Nord Powerline (-t nord-powerline) (contributed by `uselessthird <https://github.com/uselessthird>`__) Nord Powerline (-t nord-powerline) (contributed by `uselessthird <https://github.com/uselessthird>`__)
.. figure:: ../screenshots/themes/night-powerline.png
:alt: Night Powerline
Night Powerline (-t night-powerline) (contributed by `LtPeriwinkle <https://github.com/LtPeriwinkle>`__)
.. figure:: ../screenshots/themes/default.png .. figure:: ../screenshots/themes/default.png
:alt: Default :alt: Default

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -39,9 +39,6 @@ packages = find:
scripts = scripts =
./bumblebee-status ./bumblebee-status
./bumblebee-ctl ./bumblebee-ctl
./bin/load-i3-bars.sh
./bin/pacman-updates
./bin/toggle-display.sh
[versioneer] [versioneer]
VCS = git VCS = git

View file

@ -53,8 +53,10 @@ setup(
version=versioneer.get_version(), version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(), cmdclass=versioneer.get_cmdclass(),
zip_safe=False, zip_safe=False,
test_suite="tests",
data_files=[ data_files=[
("share/bumblebee-status/themes", glob.glob("themes/*.json")), ("share/bumblebee-status/themes", glob.glob("themes/*.json")),
("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")), ("share/bumblebee-status/themes/icons", glob.glob("themes/icons/*.json")),
("share/bumblebee-status/utility", glob.glob("bin/*")),
], ],
) )

View file

@ -52,8 +52,8 @@ class config(unittest.TestCase):
def test_logfile(self): def test_logfile(self):
cfg = core.config.Config(["-f", "my-custom-logfile"]) cfg = core.config.Config(["-f", "my-custom-logfile"])
self.assertEquals(None, self.defaultConfig.logfile()) self.assertEqual(None, self.defaultConfig.logfile())
self.assertEquals("my-custom-logfile", cfg.logfile()) self.assertEqual("my-custom-logfile", cfg.logfile())
def test_all_modules(self): def test_all_modules(self):
modules = core.config.all_modules() modules = core.config.all_modules()

View file

@ -7,6 +7,7 @@ import core.config
class TestModule(core.module.Module): class TestModule(core.module.Module):
@core.decorators.never
def __init__(self, config=None, theme=None): def __init__(self, config=None, theme=None):
config = core.config.Config([]) config = core.config.Config([])
super().__init__(config, theme, core.widget.Widget(self.get)) super().__init__(config, theme, core.widget.Widget(self.get))
@ -24,6 +25,10 @@ class config(unittest.TestCase):
self.width = 10 self.width = 10
self.module.set("scrolling.width", self.width) 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): def test_no_text(self):
self.assertEqual("", self.module.text) self.assertEqual("", self.module.text)
self.assertEqual("", self.module.get(self.widget)) self.assertEqual("", self.module.get(self.widget))
@ -70,5 +75,25 @@ class config(unittest.TestCase):
self.module.text = "wxyz" self.module.text = "wxyz"
self.assertEqual("wx", self.module.get(self.widget)) 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -70,7 +70,9 @@ class config(unittest.TestCase):
self.inputObject, self.someEvent["button"], self.someCommand self.inputObject, self.someEvent["button"], self.someCommand
) )
core.input.trigger(self.someEvent) 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): def test_non_existent_callback(self):
with unittest.mock.patch("core.input.util.cli") as cli: with unittest.mock.patch("core.input.util.cli") as cli:

View file

@ -6,6 +6,7 @@ import shlex
import core.module import core.module
import core.widget import core.widget
import core.config import core.config
import core.input
class TestModule(core.module.Module): 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(80, 80, 100))
self.assertEqual(None, module.threshold_state(10, 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 # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View file

@ -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"
}
}
}