[core] restructure to allow PIP packaging
OK - so I have to admit I *hate* the fact that PIP seems to require a subdirectory named like the library. But since the PIP package is something really nifty to have (thanks to @tony again!!!), I updated the codebase to hopefully conform with what PIP expects. Testruns so far look promising...
This commit is contained in:
parent
1d25be2059
commit
320827d577
146 changed files with 2509 additions and 2 deletions
0
bumblebee_status/core/__init__.py
Normal file
0
bumblebee_status/core/__init__.py
Normal file
240
bumblebee_status/core/config.py
Normal file
240
bumblebee_status/core/config.py
Normal file
|
@ -0,0 +1,240 @@
|
|||
import os
|
||||
import ast
|
||||
|
||||
from configparser import RawConfigParser
|
||||
|
||||
import sys
|
||||
import glob
|
||||
import textwrap
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import core.theme
|
||||
|
||||
import util.store
|
||||
import util.format
|
||||
|
||||
import modules.core
|
||||
import modules.contrib
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MODULE_HELP = "Specify a space-separated list of modules to load. The order of the list determines their order in the i3bar (from left to right). Use <module>:<alias> to provide an alias in case you want to load the same module multiple times, but specify different parameters."
|
||||
PARAMETER_HELP = (
|
||||
"Provide configuration parameters in the form of <module>.<key>=<value>"
|
||||
)
|
||||
THEME_HELP = "Specify the theme to use for drawing modules"
|
||||
|
||||
|
||||
def all_modules():
|
||||
"""Return a list of available modules"""
|
||||
result = {}
|
||||
|
||||
for path in [modules.core.__file__, modules.contrib.__file__]:
|
||||
path = os.path.dirname(path)
|
||||
for mod in glob.iglob("{}/*.py".format(path)):
|
||||
result[os.path.basename(mod).replace(".py", "")] = 1
|
||||
|
||||
res = list(result.keys())
|
||||
res.sort()
|
||||
return res
|
||||
|
||||
|
||||
class print_usage(argparse.Action):
|
||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||
argparse.Action.__init__(self, option_strings, dest, nargs, **kwargs)
|
||||
self._indent = " " * 2
|
||||
|
||||
def __call__(self, parser, namespace, value, option_string=None):
|
||||
if value == "modules":
|
||||
self._args = namespace
|
||||
self._format = "plain"
|
||||
self.print_modules()
|
||||
elif value == "modules-rst":
|
||||
self._args = namespace
|
||||
self._format = "rst"
|
||||
self.print_modules()
|
||||
elif value == "themes":
|
||||
self.print_themes()
|
||||
sys.exit(0)
|
||||
|
||||
def print_themes(self):
|
||||
print(", ".join(core.theme.themes()))
|
||||
|
||||
def print_modules(self):
|
||||
basepath = os.path.abspath(
|
||||
os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
||||
)
|
||||
|
||||
rst = {}
|
||||
for m in all_modules():
|
||||
try:
|
||||
module_type = "core"
|
||||
filename = os.path.join(basepath, "modules", "core", "{}.py".format(m))
|
||||
if not os.path.exists(filename):
|
||||
filename = os.path.join(
|
||||
basepath, "modules", "contrib", "{}.py".format(m)
|
||||
)
|
||||
module_type = "contrib"
|
||||
if not os.path.exists(filename):
|
||||
log.warning("module {} not found".format(m))
|
||||
continue
|
||||
|
||||
doc = None
|
||||
with open(filename) as f:
|
||||
tree = ast.parse(f.read())
|
||||
doc = ast.get_docstring(tree)
|
||||
|
||||
if not doc:
|
||||
log.warning("failed to find docstring for {}".format(m))
|
||||
continue
|
||||
if self._format == "rst":
|
||||
if os.path.exists(
|
||||
os.path.join(basepath, "screenshots", "{}.png".format(m))
|
||||
):
|
||||
doc = "{}\n\n.. image:: ../screenshots/{}.png".format(doc, m)
|
||||
|
||||
rst[module_type] = rst.get(module_type, [])
|
||||
rst[module_type].append({ "module": m, "content": doc })
|
||||
else:
|
||||
print(
|
||||
textwrap.fill(
|
||||
"{}:".format(m),
|
||||
80,
|
||||
initial_indent=self._indent * 2,
|
||||
subsequent_indent=self._indent * 2,
|
||||
)
|
||||
)
|
||||
for line in doc.split("\n"):
|
||||
print(
|
||||
textwrap.fill(
|
||||
line,
|
||||
80,
|
||||
initial_indent=self._indent * 3,
|
||||
subsequent_indent=self._indent * 6,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
log.warning(e)
|
||||
|
||||
if self._format == "rst":
|
||||
print("List of modules\n===============")
|
||||
for k in [ "core", "contrib" ]:
|
||||
print("\n{}\n{}\n".format(k, "-"*len(k)))
|
||||
for mod in rst[k]:
|
||||
print("\n{}\n{}\n".format(mod["module"], "~"*len(mod["module"])))
|
||||
print(mod["content"])
|
||||
|
||||
|
||||
class Config(util.store.Store):
|
||||
def __init__(self, args):
|
||||
super(Config, self).__init__()
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="bumblebee-status is a modular, theme-able status line generator for the i3 window manager. https://github.com/tobi-wan-kenobi/bumblebee-status/wiki"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m", "--modules", nargs="+", action="append", default=[], help=MODULE_HELP
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--parameters",
|
||||
nargs="+",
|
||||
action="append",
|
||||
default=[],
|
||||
help=PARAMETER_HELP,
|
||||
)
|
||||
parser.add_argument("-t", "--theme", default="default", help=THEME_HELP)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--iconset",
|
||||
default="auto",
|
||||
help="Specify the name of an iconset to use (overrides theme default)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--autohide",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="Specify a list of modules to hide when not in warning/error state",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--debug", action="store_true", help="Add debug fields to i3 output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--logfile",
|
||||
help="destination for the debug log file, if -d|--debug is specified; defaults to stderr",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--right-to-left",
|
||||
action="store_true",
|
||||
help="Draw widgets from right to left, rather than left to right (which is the default)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--list",
|
||||
choices=["modules", "themes", "modules-rst"],
|
||||
help="Display a list of available themes or available modules, along with their parameters",
|
||||
action=print_usage,
|
||||
)
|
||||
|
||||
self.__args = parser.parse_args(args)
|
||||
|
||||
for cfg in [
|
||||
"~/.bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status.conf",
|
||||
"~/.config/bumblebee-status/config",
|
||||
]:
|
||||
cfg = os.path.expanduser(cfg)
|
||||
self.load_config(cfg)
|
||||
|
||||
parameters = [item for sub in self.__args.parameters for item in sub]
|
||||
for param in parameters:
|
||||
if not "=" in param:
|
||||
log.error(
|
||||
'missing value for parameter "{}" - ignoring this parameter'.format(
|
||||
param
|
||||
)
|
||||
)
|
||||
continue
|
||||
key, value = param.split("=", 1)
|
||||
self.set(key, value)
|
||||
|
||||
def load_config(self, filename):
|
||||
if os.path.exists(filename):
|
||||
log.info("loading {}".format(filename))
|
||||
tmp = RawConfigParser()
|
||||
tmp.read(filename)
|
||||
|
||||
if tmp.has_section("module-parameters"):
|
||||
for key, value in tmp.items("module-parameters"):
|
||||
self.set(key, value)
|
||||
|
||||
def modules(self):
|
||||
return [item for sub in self.__args.modules for item in sub]
|
||||
|
||||
def interval(self, default=1):
|
||||
return util.format.seconds(self.get("interval", default))
|
||||
|
||||
def debug(self):
|
||||
return self.__args.debug
|
||||
|
||||
def reverse(self):
|
||||
return self.__args.right_to_left
|
||||
|
||||
def logfile(self):
|
||||
return self.__args.logfile
|
||||
|
||||
def theme(self):
|
||||
return self.__args.theme
|
||||
|
||||
def iconset(self):
|
||||
return self.__args.iconset
|
||||
|
||||
def autohide(self, name):
|
||||
return name in self.__args.autohide
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
67
bumblebee_status/core/decorators.py
Normal file
67
bumblebee_status/core/decorators.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
import util.format
|
||||
|
||||
|
||||
def never(init):
|
||||
def call_init(obj, *args, **kwargs):
|
||||
init(obj, *args, **kwargs)
|
||||
if obj.parameter("interval") is None:
|
||||
obj.set("interval", "never")
|
||||
|
||||
return call_init
|
||||
|
||||
|
||||
def every(hours=0, minutes=0, seconds=0):
|
||||
def decorator_init(init):
|
||||
def call_init(obj, *args, **kwargs):
|
||||
init(obj, *args, **kwargs)
|
||||
if obj.parameter("interval") is None:
|
||||
obj.set("interval", hours * 3600 + minutes * 60 + seconds)
|
||||
|
||||
return call_init
|
||||
|
||||
return decorator_init
|
||||
|
||||
|
||||
def scrollable(func):
|
||||
def wrapper(module, widget):
|
||||
text = func(module, widget)
|
||||
if not text:
|
||||
return text
|
||||
|
||||
if text != widget.get("__content__", text):
|
||||
widget.set("scrolling.start", 0)
|
||||
widget.set("scrolling.direction", "right")
|
||||
widget.set("__content__", text)
|
||||
|
||||
width = util.format.asint(module.parameter("scrolling.width", 30))
|
||||
if util.format.asbool(module.parameter("scrolling.makewide", True)):
|
||||
widget.set("theme.minwidth", "A" * width)
|
||||
if width < 0 or len(text) <= width:
|
||||
return text
|
||||
|
||||
start = widget.get("scrolling.start", 0)
|
||||
bounce = util.format.asbool(module.parameter("scrolling.bounce", True))
|
||||
scroll_speed = util.format.asint(module.parameter("scrolling.speed", 1))
|
||||
direction = widget.get("scrolling.direction", "right")
|
||||
|
||||
if direction == "left":
|
||||
scroll_speed = -scroll_speed
|
||||
if start + scroll_speed <= 0: # bounce back
|
||||
widget.set("scrolling.direction", "right")
|
||||
|
||||
next_start = start + scroll_speed
|
||||
if next_start + width > len(text):
|
||||
if not bounce:
|
||||
next_start = 0
|
||||
else:
|
||||
next_start = start - scroll_speed
|
||||
widget.set("scrolling.direction", "left")
|
||||
|
||||
widget.set("scrolling.start", next_start)
|
||||
|
||||
return text[start : start + width]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
29
bumblebee_status/core/event.py
Normal file
29
bumblebee_status/core/event.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
__callbacks = {}
|
||||
|
||||
|
||||
def register(event, callback, *args, **kwargs):
|
||||
cb = callback
|
||||
if len(args) + len(kwargs) > 0:
|
||||
cb = lambda: callback(*args, **kwargs)
|
||||
|
||||
__callbacks.setdefault(event, []).append(cb)
|
||||
|
||||
|
||||
def clear():
|
||||
__callbacks.clear()
|
||||
|
||||
|
||||
def trigger(event, *args, **kwargs):
|
||||
cb = __callbacks.get(event, [])
|
||||
if len(cb) == 0:
|
||||
return False
|
||||
|
||||
for callback in cb:
|
||||
if len(args) + len(kwargs) == 0:
|
||||
callback()
|
||||
else:
|
||||
callback(*args, **kwargs)
|
||||
return True
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
69
bumblebee_status/core/input.py
Normal file
69
bumblebee_status/core/input.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import uuid
|
||||
import logging
|
||||
|
||||
import core.event
|
||||
|
||||
import util.cli
|
||||
|
||||
LEFT_MOUSE = 1
|
||||
MIDDLE_MOUSE = 2
|
||||
RIGHT_MOUSE = 3
|
||||
WHEEL_UP = 4
|
||||
WHEEL_DOWN = 5
|
||||
|
||||
|
||||
def button_name(button):
|
||||
if button == LEFT_MOUSE:
|
||||
return "left-mouse"
|
||||
if button == RIGHT_MOUSE:
|
||||
return "right-mouse"
|
||||
if button == MIDDLE_MOUSE:
|
||||
return "middle-mouse"
|
||||
if button == WHEEL_UP:
|
||||
return "wheel-up"
|
||||
if button == WHEEL_DOWN:
|
||||
return "wheel-down"
|
||||
return "n/a"
|
||||
|
||||
|
||||
class Object(object):
|
||||
def __init__(self):
|
||||
super(Object, self).__init__()
|
||||
self.id = str(uuid.uuid4())
|
||||
|
||||
|
||||
def __event_id(obj_id, button):
|
||||
return "{}::{}".format(obj_id, button_name(button))
|
||||
|
||||
|
||||
def __execute(cmd):
|
||||
try:
|
||||
util.cli.execute(cmd, wait=False)
|
||||
except Exception as e:
|
||||
logging.error("failed to invoke callback: {}".format(e))
|
||||
|
||||
|
||||
def register(obj, button=None, cmd=None):
|
||||
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))
|
||||
|
||||
|
||||
def trigger(event):
|
||||
if not "button" in event:
|
||||
return
|
||||
|
||||
triggered = False
|
||||
for field in ["instance", "name"]:
|
||||
if not field in event:
|
||||
continue
|
||||
if core.event.trigger(__event_id(event[field], event["button"]), event):
|
||||
triggered = True
|
||||
if not triggered:
|
||||
core.event.trigger(__event_id("", event["button"]), event)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
124
bumblebee_status/core/module.py
Normal file
124
bumblebee_status/core/module.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
import os
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
import core.input
|
||||
import core.widget
|
||||
import core.decorators
|
||||
|
||||
try:
|
||||
error = ModuleNotFoundError("")
|
||||
except Exception as e:
|
||||
ModuleNotFoundError = Exception
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load(module_name, config=core.config.Config([]), theme=None):
|
||||
error = None
|
||||
module_short, alias = (module_name.split(":") + [module_name])[0:2]
|
||||
config.set("__alias__", alias)
|
||||
for namespace in ["core", "contrib"]:
|
||||
try:
|
||||
mod = importlib.import_module(
|
||||
"modules.{}.{}".format(namespace, module_short)
|
||||
)
|
||||
log.debug(
|
||||
"importing {} from {}.{}".format(module_short, namespace, module_short)
|
||||
)
|
||||
return getattr(mod, "Module")(config, theme)
|
||||
except ImportError as e:
|
||||
log.debug("failed to import {}: {}".format(module_name, e))
|
||||
error = e
|
||||
log.fatal("failed to import {}: {}".format(module_name, error))
|
||||
return Error(config=config, module=module_name, error=error)
|
||||
|
||||
|
||||
class Module(core.input.Object):
|
||||
def __init__(self, config=core.config.Config([]), theme=None, widgets=[]):
|
||||
super().__init__()
|
||||
self.__config = config
|
||||
self.__widgets = widgets if isinstance(widgets, list) else [widgets]
|
||||
|
||||
self.module_name = self.__module__.split(".")[-1]
|
||||
self.name = self.module_name
|
||||
self.alias = self.__config.get("__alias__", None)
|
||||
self.id = self.alias if self.alias else self.name
|
||||
self.next_update = None
|
||||
|
||||
self.theme = theme
|
||||
|
||||
for widget in self.__widgets:
|
||||
widget.module = self
|
||||
|
||||
def hidden(self):
|
||||
return False
|
||||
|
||||
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
|
||||
|
||||
def set(self, key, value):
|
||||
self.__config.set("{}.{}".format(self.name, key), value)
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def update_wrapper(self):
|
||||
try:
|
||||
self.update()
|
||||
except Exception as e:
|
||||
self.set("interval", 1)
|
||||
module = Error(config=self.__config, module="error", error=str(e))
|
||||
self.__widgets = [module.widget()]
|
||||
self.update = module.update
|
||||
|
||||
def widgets(self):
|
||||
return self.__widgets
|
||||
|
||||
def clear_widgets(self):
|
||||
del self.__widgets[:]
|
||||
|
||||
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
|
||||
|
||||
def widget(self, name=None):
|
||||
if not name:
|
||||
return self.widgets()[0]
|
||||
|
||||
for w in self.widgets():
|
||||
if w.name == name:
|
||||
return w
|
||||
return None
|
||||
|
||||
def state(self, widget):
|
||||
return []
|
||||
|
||||
def threshold_state(self, value, warn, crit):
|
||||
if value > float(self.parameter("critical", crit)):
|
||||
return "critical"
|
||||
if value > float(self.parameter("warning", warn)):
|
||||
return "warning"
|
||||
return None
|
||||
|
||||
|
||||
class Error(Module):
|
||||
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
|
||||
|
||||
def full_text(self, widget):
|
||||
return "{}: {}".format(self.__module, self.__error)
|
||||
|
||||
def state(self, widget):
|
||||
return ["critical"]
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
245
bumblebee_status/core/output.py
Normal file
245
bumblebee_status/core/output.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
import sys
|
||||
import json
|
||||
import time
|
||||
|
||||
import core.theme
|
||||
import core.event
|
||||
|
||||
import util.format
|
||||
|
||||
|
||||
def dump_json(obj):
|
||||
return obj.dict()
|
||||
|
||||
|
||||
def assign(src, dst, key, src_key=None, default=None):
|
||||
if not src_key:
|
||||
if key.startswith("_"):
|
||||
src_key = key
|
||||
else:
|
||||
src_key = key.replace("_", "-") # automagically replace _ with -
|
||||
|
||||
for k in src_key if isinstance(src_key, list) else [src_key]:
|
||||
if k in src:
|
||||
dst[key] = src[k]
|
||||
return
|
||||
if default is not None:
|
||||
dst[key] = default
|
||||
|
||||
|
||||
class block(object):
|
||||
__COMMON_THEME_FIELDS = [
|
||||
"separator",
|
||||
"separator-block-width",
|
||||
"default-separators",
|
||||
"border-top",
|
||||
"border-left",
|
||||
"border-right",
|
||||
"border-bottom",
|
||||
"fg",
|
||||
"bg",
|
||||
"padding",
|
||||
"prefix",
|
||||
"suffix",
|
||||
]
|
||||
|
||||
def __init__(self, theme, module, widget):
|
||||
self.__attributes = {}
|
||||
for key in self.__COMMON_THEME_FIELDS:
|
||||
tmp = theme.get(key, widget)
|
||||
if tmp is not None:
|
||||
self.__attributes[key] = tmp
|
||||
|
||||
self.__attributes["name"] = module.id
|
||||
self.__attributes["instance"] = widget.id
|
||||
self.__attributes["prev-bg"] = theme.get("bg", "previous")
|
||||
|
||||
def set(self, key, value):
|
||||
self.__attributes[key] = value
|
||||
|
||||
def is_pango(self, attr):
|
||||
if isinstance(attr, dict) and "pango" in attr:
|
||||
return True
|
||||
return False
|
||||
|
||||
def pangoize(self, text):
|
||||
if not self.is_pango(text):
|
||||
return text
|
||||
self.__attributes["markup"] = "pango"
|
||||
attr = dict(text["pango"])
|
||||
text = attr.get("full_text", "")
|
||||
if "full_text" in attr:
|
||||
del attr["full_text"]
|
||||
result = "<span"
|
||||
for key, value in attr.items():
|
||||
result = '{} {}="{}"'.format(result, key, value)
|
||||
result = "{}>{}</span>".format(result, text)
|
||||
return result
|
||||
|
||||
def dict(self):
|
||||
result = {}
|
||||
|
||||
assign(self.__attributes, result, "full_text", ["full_text", "separator"])
|
||||
assign(self.__attributes, result, "separator", "default-separators")
|
||||
|
||||
if "_decorator" in self.__attributes:
|
||||
assign(self.__attributes, result, "color", "bg")
|
||||
assign(self.__attributes, result, "background", "prev-bg")
|
||||
result["_decorator"] = True
|
||||
else:
|
||||
assign(self.__attributes, result, "color", "fg")
|
||||
assign(self.__attributes, result, "background", "bg")
|
||||
|
||||
if "full_text" in self.__attributes:
|
||||
result["full_text"] = self.pangoize(result["full_text"])
|
||||
result["full_text"] = self.__format(self.__attributes["full_text"])
|
||||
|
||||
for k in [
|
||||
"name",
|
||||
"instance",
|
||||
"separator_block_width",
|
||||
"border",
|
||||
"border_top",
|
||||
"border_bottom",
|
||||
"border_left",
|
||||
"border_right",
|
||||
"markup",
|
||||
"_raw",
|
||||
"_suffix",
|
||||
"_prefix",
|
||||
"min_width",
|
||||
"align",
|
||||
]:
|
||||
assign(self.__attributes, result, k)
|
||||
|
||||
return result
|
||||
|
||||
def __pad(self, text):
|
||||
padding = self.__attributes.get("padding", "")
|
||||
if not text:
|
||||
return padding
|
||||
return "{}{}{}".format(padding, text, padding)
|
||||
|
||||
def __format(self, text):
|
||||
if text is None:
|
||||
return None
|
||||
prefix = self.__pad(self.pangoize(self.__attributes.get("prefix")))
|
||||
suffix = self.__pad(self.pangoize(self.__attributes.get("suffix")))
|
||||
self.set("_prefix", prefix)
|
||||
self.set("_suffix", suffix)
|
||||
self.set("_raw", text)
|
||||
return "{}{}{}".format(prefix, text, suffix)
|
||||
|
||||
|
||||
class i3(object):
|
||||
def __init__(self, theme=core.theme.Theme(), config=core.config.Config([])):
|
||||
self.__modules = []
|
||||
self.__content = {}
|
||||
self.__theme = theme
|
||||
self.__config = config
|
||||
core.event.register("update", self.update)
|
||||
core.event.register("start", self.draw, "start")
|
||||
core.event.register("draw", self.draw, "statusline")
|
||||
core.event.register("stop", self.draw, "stop")
|
||||
|
||||
def theme(self, new_theme=None):
|
||||
if new_theme:
|
||||
self.__theme = new_theme
|
||||
return self.__theme
|
||||
|
||||
def modules(self, modules=None):
|
||||
if not modules:
|
||||
return self.__modules
|
||||
self.__modules = modules if isinstance(modules, list) else [modules]
|
||||
|
||||
def draw(self, what, args=None):
|
||||
cb = getattr(self, what)
|
||||
data = cb(args) if args else cb()
|
||||
if "blocks" in data:
|
||||
sys.stdout.write(json.dumps(data["blocks"], default=dump_json))
|
||||
if "suffix" in data:
|
||||
sys.stdout.write(data["suffix"])
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
def start(self):
|
||||
return {
|
||||
"blocks": {"version": 1, "click_events": True},
|
||||
"suffix": "\n[",
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
return {"suffix": "\n]"}
|
||||
|
||||
def __separator_block(self, module, widget):
|
||||
if not self.__theme.get("separator"):
|
||||
return []
|
||||
blk = block(self.__theme, module, widget)
|
||||
blk.set("_decorator", True)
|
||||
return [blk]
|
||||
|
||||
def __content_block(self, module, widget):
|
||||
blk = block(self.__theme, module, widget)
|
||||
minwidth = widget.theme("minwidth")
|
||||
if minwidth is not None:
|
||||
try:
|
||||
blk.set("min-width", "-" * int(minwidth))
|
||||
except:
|
||||
blk.set("min-width", minwidth)
|
||||
blk.set("align", widget.theme("align"))
|
||||
blk.set("full_text", self.__content[widget])
|
||||
if widget.get("pango", False):
|
||||
blk.set("markup", "pango")
|
||||
if self.__config.debug():
|
||||
state = module.state(widget)
|
||||
if isinstance(state, list):
|
||||
state = ", ".join(state)
|
||||
blk.set("__state", state)
|
||||
return blk
|
||||
|
||||
def blocks(self, module):
|
||||
blocks = []
|
||||
for widget in module.widgets():
|
||||
if widget.module and self.__config.autohide(widget.module.name):
|
||||
if not any(
|
||||
state in widget.state() for state in ["warning", "critical"]
|
||||
):
|
||||
continue
|
||||
if module.hidden():
|
||||
continue
|
||||
blocks.extend(self.__separator_block(module, widget))
|
||||
blocks.append(self.__content_block(module, widget))
|
||||
core.event.trigger("next-widget")
|
||||
return blocks
|
||||
|
||||
# TODO: only updates full text, not the state!?
|
||||
def update(self, affected_modules=None, redraw_only=False):
|
||||
now = time.time()
|
||||
for module in self.__modules:
|
||||
if affected_modules and not module.id in affected_modules:
|
||||
continue
|
||||
if not affected_modules and module.next_update:
|
||||
if module.parameter("interval", "") == "never":
|
||||
continue
|
||||
if now < module.next_update:
|
||||
continue
|
||||
if not redraw_only:
|
||||
module.update_wrapper()
|
||||
if module.parameter("interval", "") != "never":
|
||||
module.next_update = now + util.format.seconds(
|
||||
module.parameter("interval", self.__config.interval())
|
||||
)
|
||||
for widget in module.widgets():
|
||||
self.__content[widget] = widget.full_text()
|
||||
|
||||
def statusline(self):
|
||||
blocks = []
|
||||
for module in self.__modules:
|
||||
blocks.extend(self.blocks(module))
|
||||
return {"blocks": blocks, "suffix": ","}
|
||||
|
||||
def wait(self, interval):
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
158
bumblebee_status/core/theme.py
Normal file
158
bumblebee_status/core/theme.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
import os
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import copy
|
||||
import glob
|
||||
|
||||
import core.event
|
||||
import util.algorithm
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
THEME_BASE_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
PATHS = [
|
||||
".",
|
||||
os.path.join(THEME_BASE_DIR, "../../themes"),
|
||||
os.path.expanduser("~/.config/bumblebee-status/themes"),
|
||||
os.path.expanduser("~/.local/share/bumblebee-status/themes"), # PIP
|
||||
]
|
||||
|
||||
|
||||
def themes():
|
||||
themes_dict = {}
|
||||
|
||||
for path in PATHS:
|
||||
for filename in glob.iglob("{}/*.json".format(path)):
|
||||
if "test" not in filename:
|
||||
themes_dict[os.path.basename(filename).replace(".json", "")] = 1
|
||||
result = list(themes_dict.keys())
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
|
||||
def merge_replace(value, new_value, key):
|
||||
if not isinstance(value, dict):
|
||||
return new_value
|
||||
if isinstance(new_value, dict):
|
||||
util.algorithm.merge(value, new_value)
|
||||
return value
|
||||
# right now, merging needs explicit pango support :(
|
||||
if "pango" in value:
|
||||
value["pango"]["full_text"] = new_value
|
||||
return value
|
||||
|
||||
|
||||
class Theme(object):
|
||||
def __init__(self, name="default", iconset="auto", raw_data=None):
|
||||
self.name = name
|
||||
self.__widget_count = 0
|
||||
self.__previous = {}
|
||||
self.__current = {}
|
||||
self.__keywords = {}
|
||||
self.__value_idx = {}
|
||||
self.__data = raw_data if raw_data else self.load(name)
|
||||
for icons in self.__data.get("icons", []):
|
||||
util.algorithm.merge(self.__data, self.load(icons, "icons"))
|
||||
if iconset != "auto":
|
||||
util.algorithm.merge(self.__data, self.load(iconset, "icons"))
|
||||
for colors in self.__data.get("colors", []):
|
||||
util.algorithm.merge(self.__keywords, self.load_keywords(colors))
|
||||
|
||||
core.event.register("draw", self.__start)
|
||||
core.event.register("next-widget", self.__next_widget)
|
||||
|
||||
def keywords(self):
|
||||
return self.__keywords
|
||||
|
||||
def color(self, name, default=None):
|
||||
return self.keywords().get(name, default)
|
||||
|
||||
def load(self, name, subdir=""):
|
||||
if isinstance(name, dict):
|
||||
return name # support plain data
|
||||
for path in PATHS:
|
||||
theme_file = os.path.join(path, subdir, "{}.json".format(name))
|
||||
result = self.__load_json(theme_file)
|
||||
if result != {}:
|
||||
return result
|
||||
raise RuntimeError("unable to find theme {}".format(name))
|
||||
|
||||
def __load_json(self, filename):
|
||||
filename = os.path.expanduser(filename)
|
||||
if not os.path.isfile(filename):
|
||||
return {}
|
||||
with io.open(filename) as data:
|
||||
return json.load(data)
|
||||
|
||||
def load_keywords(self, name):
|
||||
try:
|
||||
if isinstance(name, dict):
|
||||
return name
|
||||
if name.lower() == "wal":
|
||||
wal = self.__load_json("~/.cache/wal/colors.json")
|
||||
result = {}
|
||||
for field in ["special", "colors"]:
|
||||
for key in wal.get(field, {}):
|
||||
result[key] = wal[field][key]
|
||||
return result
|
||||
except Exception as e:
|
||||
log.error("failed to load colors: {}", e)
|
||||
|
||||
def __start(self):
|
||||
self.__widget_count = 0
|
||||
self.__current.clear()
|
||||
self.__previous.clear()
|
||||
|
||||
for key, value in self.__value_idx.items():
|
||||
self.__value_idx[key] = value + 1
|
||||
|
||||
def __next_widget(self):
|
||||
self.__widget_count = self.__widget_count + 1
|
||||
self.__previous = dict(self.__current)
|
||||
self.__current.clear()
|
||||
|
||||
def get(self, key, widget=None, default=None):
|
||||
if not widget:
|
||||
widget = core.widget.Widget("")
|
||||
# special handling
|
||||
if widget == "previous":
|
||||
return self.__previous.get(key, None)
|
||||
|
||||
value = default
|
||||
|
||||
for option in ["defaults", "cycle"]:
|
||||
if option in self.__data:
|
||||
tmp = self.__data[option]
|
||||
if isinstance(tmp, list):
|
||||
tmp = tmp[self.__widget_count % len(tmp)]
|
||||
value = merge_replace(value, tmp.get(key, value), key)
|
||||
|
||||
if isinstance(value, dict):
|
||||
value = copy.deepcopy(value)
|
||||
|
||||
value = merge_replace(value, self.__data.get(key, value), key)
|
||||
|
||||
if widget.module:
|
||||
value = merge_replace(
|
||||
value, self.get(widget.module.name, None, {}).get(key, value), key
|
||||
)
|
||||
|
||||
if not key in widget.state():
|
||||
for state in widget.state():
|
||||
theme = self.get(state, widget, {})
|
||||
value = merge_replace(value, theme.get(key, value), key)
|
||||
|
||||
if not type(value) in (list, dict):
|
||||
value = self.__keywords.get(value, value)
|
||||
|
||||
if isinstance(value, list):
|
||||
idx = self.__value_idx.get("{}::{}".format(widget.id, key), 0) % len(value)
|
||||
self.__value_idx["{}::{}".format(widget.id, key)] = idx
|
||||
widget.set(key, idx)
|
||||
value = value[idx]
|
||||
self.__current[key] = value
|
||||
return value
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
69
bumblebee_status/core/widget.py
Normal file
69
bumblebee_status/core/widget.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import core.input
|
||||
import core.decorators
|
||||
|
||||
import util.store
|
||||
import util.format
|
||||
|
||||
|
||||
class Widget(util.store.Store, core.input.Object):
|
||||
def __init__(self, full_text="", name=None, module=None):
|
||||
super(Widget, self).__init__()
|
||||
self.__full_text = full_text
|
||||
self.module = module
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
return self.__module
|
||||
|
||||
@module.setter
|
||||
def module(self, module):
|
||||
self.__module = module
|
||||
|
||||
if self.index() < 0:
|
||||
return
|
||||
|
||||
if module:
|
||||
custom_ids = util.format.aslist(module.parameter("id"))
|
||||
if len(custom_ids) > self.index():
|
||||
self.id = custom_ids[self.index()]
|
||||
|
||||
def index(self):
|
||||
if not self.module:
|
||||
return 0
|
||||
|
||||
idx = 0
|
||||
for w in self.module.widgets():
|
||||
if w.id == self.id:
|
||||
return idx
|
||||
idx = idx + 1
|
||||
return -1 # not found
|
||||
|
||||
def theme(self, attribute):
|
||||
attr = "theme.{}".format(attribute)
|
||||
if self.module:
|
||||
param = util.format.aslist(self.module.parameter(attr))
|
||||
if param and len(param) > self.index():
|
||||
return param[self.index()]
|
||||
return self.get(attr)
|
||||
|
||||
def full_text(self, value=None):
|
||||
if value:
|
||||
self.__full_text = value
|
||||
else:
|
||||
if callable(self.__full_text):
|
||||
return self.__full_text(self)
|
||||
return self.__full_text
|
||||
|
||||
def state(self):
|
||||
rv = []
|
||||
if self.get("state", None):
|
||||
tmp = self.get("state")
|
||||
rv = tmp[:] if isinstance(tmp, list) else [tmp]
|
||||
if self.module:
|
||||
tmp = self.module.state(self)
|
||||
rv.extend(tmp if isinstance(tmp, list) else [tmp])
|
||||
return rv
|
||||
|
||||
|
||||
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|
Loading…
Add table
Add a link
Reference in a new issue