5d1d994dce
In a theme file, it is now possible to provide an array of "color definitions", which allow you to use names instead of colors throughout the theme file. Currently, only the colorset "wal" is supported, which reads all colors from the wal JSON file (~/.cache/wal/colors.json) and makes them usable in the theme (as "foreground", "background", "cursor", "color12", etc.). An example of this can be found in the theme wal-powerline. see #185
201 lines
6.5 KiB
Python
201 lines
6.5 KiB
Python
# pylint: disable=C0103
|
|
|
|
"""Theme support"""
|
|
|
|
import os
|
|
import glob
|
|
import copy
|
|
import json
|
|
import io
|
|
|
|
import bumblebee.error
|
|
|
|
def theme_path():
|
|
"""Return the path of the theme directory"""
|
|
return os.path.dirname("{}/../themes/".format(os.path.dirname(os.path.realpath(__file__))))
|
|
|
|
def themes():
|
|
result = []
|
|
|
|
for filename in glob.iglob("{}/*.json".format(theme_path())):
|
|
if "test" not in filename:
|
|
result.append(os.path.basename(filename).replace(".json", ""))
|
|
return result
|
|
|
|
class Theme(object):
|
|
"""Represents a collection of icons and colors"""
|
|
def __init__(self, name):
|
|
self._widget = None
|
|
self._cycle_idx = 0
|
|
self._cycle = {}
|
|
self._prevbg = None
|
|
self._colorset = {}
|
|
self._init(self.load(name))
|
|
|
|
def _init(self, data):
|
|
"""Initialize theme from data structure"""
|
|
self._theme = data
|
|
for iconset in data.get("icons", []):
|
|
self._merge(data, self._load_icons(iconset))
|
|
for colorset in data.get("colors", []):
|
|
self._merge(self._colorset, self._load_colors(colorset))
|
|
self._defaults = data.get("defaults", {})
|
|
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"""
|
|
self._cycle = self._cycles[0] if len(self._cycles) > 0 else {}
|
|
self._cycle_idx = 0
|
|
self._widget = None
|
|
self._prevbg = None
|
|
|
|
def padding(self, widget):
|
|
"""Return padding for widget"""
|
|
return self._get(widget, "padding", "")
|
|
|
|
def prefix(self, widget, default=None):
|
|
"""Return the theme prefix for a widget's full text"""
|
|
padding = self.padding(widget)
|
|
pre = self._get(widget, "prefix", None)
|
|
return u"{}{}{}".format(padding, pre, padding) if pre else default
|
|
|
|
def suffix(self, widget, default=None):
|
|
"""Return the theme suffix for a widget's full text"""
|
|
padding = self._get(widget, "padding", "")
|
|
suf = self._get(widget, "suffix", None)
|
|
return u"{}{}{}".format(padding, suf, padding) if suf else default
|
|
|
|
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)
|
|
|
|
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", "")
|
|
|
|
def separator(self, widget):
|
|
"""Return the separator between widgets"""
|
|
return self._get(widget, "separator", None)
|
|
|
|
def separator_fg(self, widget):
|
|
"""Return the separator's foreground/text color"""
|
|
return self.bg(widget)
|
|
|
|
def separator_bg(self, widget):
|
|
"""Return the separator's background color"""
|
|
return self._prevbg
|
|
|
|
def separator_block_width(self, widget):
|
|
"""Return the SBW"""
|
|
return self._get(widget, "separator-block-width", None)
|
|
|
|
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
|
|
|
|
def _load_colors(self, name):
|
|
"""Load colors for a theme"""
|
|
if name == "wal":
|
|
return self._load_wal_colors()
|
|
|
|
def _load_icons(self, name):
|
|
"""Load icons for a theme"""
|
|
path = "{}/icons/".format(theme_path())
|
|
return self.load(name, path=path)
|
|
|
|
def load(self, name, path=theme_path()):
|
|
"""Load and parse a theme file"""
|
|
themefile = "{}/{}.json".format(path, name)
|
|
|
|
if os.path.isfile(themefile):
|
|
try:
|
|
with io.open(themefile, encoding="utf-8") as data:
|
|
return json.load(data)
|
|
except ValueError as exception:
|
|
raise bumblebee.error.ThemeLoadError("JSON error: {}".format(exception))
|
|
else:
|
|
raise bumblebee.error.ThemeLoadError("no such theme: {}".format(name))
|
|
|
|
def _get(self, widget, name, default=None):
|
|
"""Return the config value 'name' for 'widget'"""
|
|
|
|
if not self._widget:
|
|
self._widget = widget
|
|
|
|
if self._widget != widget:
|
|
self._prevbg = self.bg(self._widget)
|
|
self._widget = widget
|
|
if len(self._cycles) > 0:
|
|
self._cycle_idx = (self._cycle_idx + 1) % len(self._cycles)
|
|
self._cycle = self._cycles[self._cycle_idx]
|
|
|
|
module_theme = self._theme.get(widget.module, {})
|
|
class_theme = self._theme.get(widget.cls(), {})
|
|
|
|
state_themes = []
|
|
# avoid infinite recursion
|
|
states = widget.state()
|
|
if name not in states:
|
|
for state in states:
|
|
state_themes.append(self._get(widget, state, {}))
|
|
|
|
value = self._defaults.get(name, default)
|
|
value = widget.get("theme.{}".format(name), value)
|
|
value = self._cycle.get(name, value)
|
|
value = class_theme.get(name, value)
|
|
value = module_theme.get(name, value)
|
|
|
|
for theme in state_themes:
|
|
value = theme.get(name, value)
|
|
|
|
if isinstance(value, list):
|
|
key = "{}-idx".format(name)
|
|
idx = widget.get(key, 0)
|
|
widget.set(key, (idx + 1) % len(value))
|
|
value = value[idx]
|
|
|
|
if isinstance(value, list) or isinstance(value, dict):
|
|
return value
|
|
return self._colorset.get(value, value)
|
|
|
|
# algorithm copied from
|
|
# http://blog.impressiver.com/post/31434674390/deep-merge-multiple-python-dicts
|
|
# nicely done :)
|
|
def _merge(self, target, *args):
|
|
"""Merge two arbitrarily nested data structures"""
|
|
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:
|
|
if not key in target:
|
|
target[key] = copy.deepcopy(value)
|
|
return target
|
|
|
|
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
|