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"
exclude_paths:
- 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
.vscode/
# mypy cache
.mypy_cache

View file

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

View file

@ -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()

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():
"""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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

@ -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("<FocusOut>", self._on_focus_out)
self._menu.bind("<FocusOut>", self.__on_focus_out)
else:
self._root = parent.root()
self._root.withdraw()
self._menu = tk.Menu(self._root, tearoff=0)
self._menu.bind("<FocusOut>", self._on_focus_out)
self._menu.bind("<FocusOut>", self.__on_focus_out)
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):
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)

View file

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

View file

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

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>`__)
.. 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
:alt: Default

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

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

View file

@ -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/*")),
],
)

View file

@ -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()

View file

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

View file

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

View file

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

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