Merge branch 'development' into arandr
This commit is contained in:
commit
2fcd277cf9
34 changed files with 713 additions and 270 deletions
|
@ -0,0 +1,3 @@
|
|||
import bumblebee_status.discover
|
||||
|
||||
bumblebee_status.discover.discover()
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue