2016-12-09 11:57:13 +01:00
|
|
|
# pylint: disable=C0103
|
|
|
|
|
2016-12-04 18:10:04 +01:00
|
|
|
"""Theme support"""
|
|
|
|
|
2016-12-08 09:44:05 +01:00
|
|
|
import os
|
2016-12-17 07:05:23 +01:00
|
|
|
import glob
|
2016-12-08 12:09:21 +01:00
|
|
|
import copy
|
2016-12-08 09:44:05 +01:00
|
|
|
import json
|
2017-05-13 16:41:43 +02:00
|
|
|
import io
|
2017-12-29 14:49:13 +01:00
|
|
|
import re
|
|
|
|
import logging
|
2020-02-05 01:06:26 +01:00
|
|
|
import pkg_resources
|
2016-12-08 09:44:05 +01:00
|
|
|
|
|
|
|
import bumblebee.error
|
|
|
|
|
|
|
|
def theme_path():
|
|
|
|
"""Return the path of the theme directory"""
|
2017-11-05 09:08:01 +01:00
|
|
|
return [
|
2020-02-05 01:06:26 +01:00
|
|
|
os.path.realpath(x) for x in [
|
|
|
|
os.path.dirname("{}/../themes/".format(os.path.dirname(os.path.realpath(__file__)))),
|
|
|
|
os.path.dirname(
|
|
|
|
"{}/../../../../share/bumblebee-status/themes/".format(
|
|
|
|
os.path.dirname(os.path.realpath(__file__)))),
|
|
|
|
os.path.dirname(os.path.expanduser("~/.config/bumblebee-status/themes/")),
|
|
|
|
pkg_resources.resource_filename('bumblebee', 'themes')] if os.path.exists(x)
|
2017-11-05 09:08:01 +01:00
|
|
|
]
|
2016-12-08 09:44:05 +01:00
|
|
|
|
2016-12-17 07:05:23 +01:00
|
|
|
def themes():
|
2020-02-05 01:06:26 +01:00
|
|
|
themes_dict = {}
|
2016-12-17 07:05:23 +01:00
|
|
|
|
2017-11-05 09:08:01 +01:00
|
|
|
for path in theme_path():
|
|
|
|
for filename in glob.iglob("{}/*.json".format(path)):
|
|
|
|
if "test" not in filename:
|
2020-02-05 01:06:26 +01:00
|
|
|
themes_dict[os.path.basename(filename).replace(".json", "")] = 1
|
|
|
|
result = list(themes_dict.keys())
|
2017-11-18 14:51:25 +01:00
|
|
|
result.sort()
|
2016-12-17 07:05:23 +01:00
|
|
|
return result
|
|
|
|
|
2016-12-04 18:10:04 +01:00
|
|
|
class Theme(object):
|
2016-12-08 08:44:54 +01:00
|
|
|
"""Represents a collection of icons and colors"""
|
2018-04-29 21:11:58 +02:00
|
|
|
def __init__(self, name, iconset="auto"):
|
2016-12-09 12:28:39 +01:00
|
|
|
self._widget = None
|
2016-12-09 16:33:29 +01:00
|
|
|
self._cycle_idx = 0
|
|
|
|
self._cycle = {}
|
|
|
|
self._prevbg = None
|
2017-10-08 08:13:10 +02:00
|
|
|
self._colorset = {}
|
2018-04-29 16:12:39 +02:00
|
|
|
self._iconset = iconset
|
2017-12-29 14:49:13 +01:00
|
|
|
|
|
|
|
self.load_symbols()
|
|
|
|
|
2017-11-26 19:56:57 +01:00
|
|
|
data = self.load(name)
|
|
|
|
if not data:
|
|
|
|
raise bumblebee.error.ThemeLoadError("no such theme")
|
|
|
|
self._init(data)
|
2016-12-08 09:44:05 +01:00
|
|
|
|
2017-12-29 14:49:13 +01:00
|
|
|
def load_symbols(self):
|
|
|
|
self._symbols = {}
|
|
|
|
path = os.path.expanduser("~/.config/bumblebee-status/")
|
|
|
|
try:
|
|
|
|
os.makedirs(path)
|
2020-02-05 01:06:26 +01:00
|
|
|
except OSError:
|
2017-12-29 14:49:13 +01:00
|
|
|
pass
|
|
|
|
try:
|
2018-01-03 15:58:57 +01:00
|
|
|
if os.path.exists("{}/symbols.json".format(path)):
|
|
|
|
data = json.load(io.open("{}/symbols.json".format(path)))
|
|
|
|
self._symbols = {}
|
|
|
|
for icon in data["icons"]:
|
|
|
|
code = int(icon["unicode"], 16)
|
|
|
|
try:
|
|
|
|
code = unichr(code)
|
|
|
|
except Exception:
|
|
|
|
code = chr(code)
|
|
|
|
self._symbols["${{{}}}".format(icon["id"])] = code
|
|
|
|
self._symbols["${{{}}}".format(icon["name"])] = code
|
2020-02-05 01:06:26 +01:00
|
|
|
except Exception as err:
|
|
|
|
logging.error("failed to load symbols: %s", err)
|
2017-12-29 14:49:13 +01:00
|
|
|
|
2016-12-08 11:52:47 +01:00
|
|
|
def _init(self, data):
|
|
|
|
"""Initialize theme from data structure"""
|
2017-09-20 08:59:23 +02:00
|
|
|
self._theme = data
|
2018-04-29 16:12:39 +02:00
|
|
|
if self._iconset != "auto":
|
|
|
|
self._merge(data, self._load_icons(self._iconset))
|
|
|
|
else:
|
|
|
|
for iconset in data.get("icons", []):
|
|
|
|
self._merge(data, self._load_icons(iconset))
|
2017-10-08 08:13:10 +02:00
|
|
|
for colorset in data.get("colors", []):
|
|
|
|
self._merge(self._colorset, self._load_colors(colorset))
|
2016-12-08 11:52:47 +01:00
|
|
|
self._defaults = data.get("defaults", {})
|
2016-12-09 12:28:39 +01:00
|
|
|
self._cycles = self._theme.get("cycle", [])
|
|
|
|
self.reset()
|
|
|
|
|
|
|
|
def data(self):
|
|
|
|
"""Return the raw theme data"""
|
|
|
|
return self._theme
|
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
"""Reset theme to initial state"""
|
2020-02-05 01:06:26 +01:00
|
|
|
self._cycle = self._cycles[0] if self._cycles else {}
|
2016-12-09 12:28:39 +01:00
|
|
|
self._cycle_idx = 0
|
|
|
|
self._widget = None
|
2016-12-09 12:55:16 +01:00
|
|
|
self._prevbg = None
|
2016-12-08 11:52:47 +01:00
|
|
|
|
2018-05-30 10:42:31 +02:00
|
|
|
def icon(self, widget):
|
|
|
|
icon = self._get(widget, "icon", None)
|
2018-06-04 14:50:51 +02:00
|
|
|
if icon is None:
|
2018-05-30 10:42:31 +02:00
|
|
|
return self._get(widget, "prefix", None)
|
2020-02-05 01:06:26 +01:00
|
|
|
return None
|
2018-05-30 10:42:31 +02:00
|
|
|
|
2019-08-01 20:41:13 +02:00
|
|
|
def get(self, widget, attribute, default_value=""):
|
|
|
|
return self._get(widget, attribute, default_value)
|
|
|
|
|
2016-12-09 13:06:08 +01:00
|
|
|
def padding(self, widget):
|
|
|
|
"""Return padding for widget"""
|
|
|
|
return self._get(widget, "padding", "")
|
|
|
|
|
|
|
|
def prefix(self, widget, default=None):
|
2016-12-08 11:31:20 +01:00
|
|
|
"""Return the theme prefix for a widget's full text"""
|
2016-12-09 13:06:08 +01:00
|
|
|
padding = self.padding(widget)
|
2016-12-09 12:28:39 +01:00
|
|
|
pre = self._get(widget, "prefix", None)
|
2016-12-09 13:06:08 +01:00
|
|
|
return u"{}{}{}".format(padding, pre, padding) if pre else default
|
2016-12-08 11:31:20 +01:00
|
|
|
|
2020-01-11 13:54:53 +01:00
|
|
|
def prefix_fg(self, widget):
|
|
|
|
"""Return the foreground color for the prefix"""
|
|
|
|
return self._get(widget, "prefixfg", None)
|
|
|
|
|
|
|
|
def prefix_bg(self, widget):
|
|
|
|
"""Return the background color for the prefix"""
|
|
|
|
return self._get(widget, "prefixbg", None)
|
|
|
|
|
|
|
|
def suffix_fg(self, widget):
|
|
|
|
"""Return the foreground color for the suffix"""
|
|
|
|
return self._get(widget, "suffixfg", None)
|
|
|
|
|
|
|
|
def suffix_bg(self, widget):
|
|
|
|
"""Return the background color for the suffix"""
|
|
|
|
return self._get(widget, "suffixbg", None)
|
|
|
|
|
2019-01-20 14:43:27 +01:00
|
|
|
def symbol(self, widget, name, default=None):
|
|
|
|
return self._get(widget, name, default)
|
|
|
|
|
2016-12-09 13:06:08 +01:00
|
|
|
def suffix(self, widget, default=None):
|
2016-12-08 11:31:20 +01:00
|
|
|
"""Return the theme suffix for a widget's full text"""
|
2016-12-09 12:28:39 +01:00
|
|
|
padding = self._get(widget, "padding", "")
|
|
|
|
suf = self._get(widget, "suffix", None)
|
2016-12-09 13:06:08 +01:00
|
|
|
return u"{}{}{}".format(padding, suf, padding) if suf else default
|
2016-12-08 11:52:47 +01:00
|
|
|
|
2016-12-09 08:58:45 +01:00
|
|
|
def fg(self, widget):
|
|
|
|
"""Return the foreground color for this widget"""
|
|
|
|
return self._get(widget, "fg", None)
|
|
|
|
|
|
|
|
def bg(self, widget):
|
|
|
|
"""Return the background color for this widget"""
|
|
|
|
return self._get(widget, "bg", None)
|
|
|
|
|
2017-04-22 08:24:52 +02:00
|
|
|
def align(self, widget):
|
|
|
|
"""Return the widget alignment"""
|
|
|
|
return self._get(widget, "align", None)
|
|
|
|
|
|
|
|
def minwidth(self, widget):
|
|
|
|
"""Return the minimum width string for this widget"""
|
|
|
|
return self._get(widget, "minwidth", "")
|
|
|
|
|
2016-12-09 12:55:16 +01:00
|
|
|
def separator(self, widget):
|
|
|
|
"""Return the separator between widgets"""
|
|
|
|
return self._get(widget, "separator", None)
|
|
|
|
|
|
|
|
def separator_fg(self, widget):
|
2016-12-09 16:33:29 +01:00
|
|
|
"""Return the separator's foreground/text color"""
|
2016-12-09 12:55:16 +01:00
|
|
|
return self.bg(widget)
|
|
|
|
|
|
|
|
def separator_bg(self, widget):
|
2016-12-09 16:33:29 +01:00
|
|
|
"""Return the separator's background color"""
|
2016-12-09 12:55:16 +01:00
|
|
|
return self._prevbg
|
|
|
|
|
|
|
|
def separator_block_width(self, widget):
|
|
|
|
"""Return the SBW"""
|
|
|
|
return self._get(widget, "separator-block-width", None)
|
|
|
|
|
2017-10-08 08:13:10 +02:00
|
|
|
def _load_wal_colors(self):
|
|
|
|
walfile = os.path.expanduser("~/.cache/wal/colors.json")
|
|
|
|
result = {}
|
|
|
|
with io.open(walfile) as data:
|
|
|
|
colors = json.load(data)
|
|
|
|
for field in ["special", "colors"]:
|
|
|
|
for key in colors[field]:
|
|
|
|
result[key] = colors[field][key]
|
|
|
|
return result
|
|
|
|
|
2020-02-19 21:39:44 +01:00
|
|
|
def color(self, color_name, default=None):
|
|
|
|
return self._colorset.get(color_name, default)
|
|
|
|
|
2017-10-08 08:13:10 +02:00
|
|
|
def _load_colors(self, name):
|
|
|
|
"""Load colors for a theme"""
|
2018-05-30 09:22:53 +02:00
|
|
|
try:
|
2020-02-19 21:39:44 +01:00
|
|
|
if isinstance(name, dict):
|
|
|
|
return name
|
2020-02-05 01:06:26 +01:00
|
|
|
if name.lower() == "wal":
|
2018-05-30 09:22:53 +02:00
|
|
|
return self._load_wal_colors()
|
2020-02-05 01:06:26 +01:00
|
|
|
except Exception as err:
|
|
|
|
logging.error("failed to load colors: %s", err)
|
2017-10-08 08:13:10 +02:00
|
|
|
|
2016-12-08 12:09:21 +01:00
|
|
|
def _load_icons(self, name):
|
2016-12-09 07:11:23 +01:00
|
|
|
"""Load icons for a theme"""
|
2017-11-05 09:08:01 +01:00
|
|
|
result = {}
|
|
|
|
for path in theme_path():
|
|
|
|
self._merge(result, self.load(name, path="{}/icons/".format(path)))
|
2017-12-29 14:49:13 +01:00
|
|
|
|
|
|
|
return self._replace_symbols(result)
|
|
|
|
|
|
|
|
def _replace_symbols(self, data):
|
|
|
|
rep = json.dumps(data)
|
|
|
|
tokens = re.findall(r"\${[^}]+}", rep)
|
|
|
|
for token in tokens:
|
|
|
|
rep = rep.replace(token, self._symbols[token])
|
|
|
|
return json.loads(rep)
|
2016-12-08 12:09:21 +01:00
|
|
|
|
|
|
|
def load(self, name, path=theme_path()):
|
2016-12-08 09:44:05 +01:00
|
|
|
"""Load and parse a theme file"""
|
2017-11-18 14:56:44 +01:00
|
|
|
result = None
|
2018-11-06 21:09:26 +01:00
|
|
|
|
|
|
|
full_name = os.path.expanduser(name)
|
|
|
|
if os.path.isfile(full_name):
|
|
|
|
path = os.path.dirname(full_name)
|
|
|
|
name = os.path.basename(full_name)
|
2020-02-05 01:06:26 +01:00
|
|
|
name, _, _ = name.rpartition(".json")
|
2018-11-06 21:09:26 +01:00
|
|
|
return self.load(name, path)
|
|
|
|
|
2017-11-05 09:08:01 +01:00
|
|
|
if not isinstance(path, list):
|
|
|
|
path = [path]
|
|
|
|
for p in path:
|
|
|
|
themefile = "{}/{}.json".format(p, name)
|
|
|
|
|
|
|
|
if os.path.isfile(themefile):
|
|
|
|
try:
|
|
|
|
with io.open(themefile, encoding="utf-8") as data:
|
2017-11-18 14:56:44 +01:00
|
|
|
if result is None:
|
|
|
|
result = json.load(data)
|
|
|
|
else:
|
|
|
|
self._merge(result, json.load(data))
|
2017-11-05 09:08:01 +01:00
|
|
|
except ValueError as exception:
|
|
|
|
raise bumblebee.error.ThemeLoadError("JSON error: {}".format(exception))
|
|
|
|
|
2017-11-18 14:56:44 +01:00
|
|
|
return result
|
2016-12-04 18:10:04 +01:00
|
|
|
|
2016-12-08 12:09:21 +01:00
|
|
|
def _get(self, widget, name, default=None):
|
2016-12-09 07:11:23 +01:00
|
|
|
"""Return the config value 'name' for 'widget'"""
|
2016-12-08 12:09:21 +01:00
|
|
|
|
2016-12-09 12:28:39 +01:00
|
|
|
if not self._widget:
|
|
|
|
self._widget = widget
|
|
|
|
|
2019-12-11 12:23:57 +01:00
|
|
|
if name in bumblebee.util.aslist(self._widget.get("theme.exclude", "")):
|
2019-06-27 20:21:53 +02:00
|
|
|
return None
|
|
|
|
|
2016-12-09 12:28:39 +01:00
|
|
|
if self._widget != widget:
|
2016-12-09 12:55:16 +01:00
|
|
|
self._prevbg = self.bg(self._widget)
|
2016-12-09 12:28:39 +01:00
|
|
|
self._widget = widget
|
2020-02-05 01:06:26 +01:00
|
|
|
if self._cycles:
|
2016-12-09 12:55:16 +01:00
|
|
|
self._cycle_idx = (self._cycle_idx + 1) % len(self._cycles)
|
|
|
|
self._cycle = self._cycles[self._cycle_idx]
|
2016-12-09 12:28:39 +01:00
|
|
|
|
|
|
|
module_theme = self._theme.get(widget.module, {})
|
2017-05-10 20:01:29 +02:00
|
|
|
class_theme = self._theme.get(widget.cls(), {})
|
2016-12-10 11:25:02 +01:00
|
|
|
|
|
|
|
state_themes = []
|
|
|
|
# avoid infinite recursion
|
2016-12-11 07:28:15 +01:00
|
|
|
states = widget.state()
|
|
|
|
if name not in states:
|
|
|
|
for state in states:
|
2019-07-02 20:19:26 +02:00
|
|
|
if state:
|
|
|
|
state_themes.append(self._get(widget, state, {}))
|
2016-12-08 12:44:52 +01:00
|
|
|
|
2016-12-08 12:09:21 +01:00
|
|
|
value = self._defaults.get(name, default)
|
2017-04-22 13:07:50 +02:00
|
|
|
value = widget.get("theme.{}".format(name), value)
|
2016-12-09 12:28:39 +01:00
|
|
|
value = self._cycle.get(name, value)
|
2017-05-10 20:01:29 +02:00
|
|
|
value = class_theme.get(name, value)
|
2016-12-08 12:09:21 +01:00
|
|
|
value = module_theme.get(name, value)
|
2016-12-10 11:25:02 +01:00
|
|
|
|
|
|
|
for theme in state_themes:
|
|
|
|
value = theme.get(name, value)
|
2016-12-08 11:52:47 +01:00
|
|
|
|
2016-12-10 12:00:08 +01:00
|
|
|
if isinstance(value, list):
|
|
|
|
key = "{}-idx".format(name)
|
|
|
|
idx = widget.get(key, 0)
|
|
|
|
widget.set(key, (idx + 1) % len(value))
|
|
|
|
value = value[idx]
|
|
|
|
|
2018-01-17 19:29:11 +01:00
|
|
|
mod = widget.get_module()
|
|
|
|
if mod and not mod.parameter("is-unittest"):
|
2018-01-17 18:56:12 +01:00
|
|
|
value = widget.get_module().parameter("theme.{}".format(name), value)
|
2018-01-17 18:49:09 +01:00
|
|
|
|
2020-02-05 01:06:26 +01:00
|
|
|
if isinstance(value, (dict, list)):
|
2017-10-08 08:13:10 +02:00
|
|
|
return value
|
2020-02-19 21:39:44 +01:00
|
|
|
return self.color(value, value)
|
2016-12-08 11:52:47 +01:00
|
|
|
|
2016-12-08 12:09:21 +01:00
|
|
|
# algorithm copied from
|
|
|
|
# http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts
|
|
|
|
# nicely done :)
|
|
|
|
def _merge(self, target, *args):
|
2016-12-09 07:11:23 +01:00
|
|
|
"""Merge two arbitrarily nested data structures"""
|
2016-12-08 12:09:21 +01:00
|
|
|
if len(args) > 1:
|
|
|
|
for item in args:
|
|
|
|
self._merge(item)
|
|
|
|
return target
|
|
|
|
|
|
|
|
item = args[0]
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
return item
|
|
|
|
for key, value in item.items():
|
|
|
|
if key in target and isinstance(target[key], dict):
|
|
|
|
self._merge(target[key], value)
|
|
|
|
else:
|
2017-09-20 08:59:23 +02:00
|
|
|
if not key in target:
|
|
|
|
target[key] = copy.deepcopy(value)
|
2016-12-08 12:09:21 +01:00
|
|
|
return target
|
|
|
|
|
2016-12-04 18:10:04 +01:00
|
|
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|