290f95d6b4
When pressing the middle mouse button (and it's not assigned to any other functionality), the module (i.e. all widgets of that module) will disappear and be replaced with the module's icon (or prefix, as fallback) and an ellipsis. fixes #264
276 lines
9 KiB
Python
276 lines
9 KiB
Python
# pylint: disable=C0103
|
|
|
|
"""Theme support"""
|
|
|
|
import os
|
|
import glob
|
|
import copy
|
|
import json
|
|
import io
|
|
import re
|
|
import logging
|
|
|
|
try:
|
|
import requests
|
|
from requests.exceptions import RequestException
|
|
except ImportError:
|
|
pass
|
|
|
|
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__)))),
|
|
os.path.dirname(os.path.expanduser("~/.config/bumblebee-status/themes/")),
|
|
]
|
|
|
|
def themes():
|
|
themes = {}
|
|
|
|
for path in theme_path():
|
|
for filename in glob.iglob("{}/*.json".format(path)):
|
|
if "test" not in filename:
|
|
themes[os.path.basename(filename).replace(".json", "")] = 1
|
|
result = list(themes.keys())
|
|
result.sort()
|
|
return result
|
|
|
|
class Theme(object):
|
|
"""Represents a collection of icons and colors"""
|
|
def __init__(self, name, iconset="auto"):
|
|
self._widget = None
|
|
self._cycle_idx = 0
|
|
self._cycle = {}
|
|
self._prevbg = None
|
|
self._colorset = {}
|
|
self._iconset = iconset
|
|
|
|
self.load_symbols()
|
|
|
|
data = self.load(name)
|
|
if not data:
|
|
raise bumblebee.error.ThemeLoadError("no such theme")
|
|
self._init(data)
|
|
|
|
def load_symbols(self):
|
|
self._symbols = {}
|
|
path = os.path.expanduser("~/.config/bumblebee-status/")
|
|
try:
|
|
os.makedirs(path)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
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
|
|
except Exception as e:
|
|
logging.error("failed to load symbols: {}".format(str(e)))
|
|
|
|
def _init(self, data):
|
|
"""Initialize theme from data structure"""
|
|
self._theme = data
|
|
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))
|
|
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 icon(self, widget):
|
|
icon = self._get(widget, "icon", None)
|
|
if icon == None:
|
|
return self._get(widget, "prefix", 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"""
|
|
try:
|
|
if name == "wal":
|
|
return self._load_wal_colors()
|
|
except Exception as e:
|
|
logging.error("failed to load colors: {}".format(str(e)))
|
|
|
|
def _load_icons(self, name):
|
|
"""Load icons for a theme"""
|
|
result = {}
|
|
for path in theme_path():
|
|
self._merge(result, self.load(name, path="{}/icons/".format(path)))
|
|
|
|
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)
|
|
|
|
def load(self, name, path=theme_path()):
|
|
"""Load and parse a theme file"""
|
|
result = None
|
|
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:
|
|
if result is None:
|
|
result = json.load(data)
|
|
else:
|
|
self._merge(result, json.load(data))
|
|
except ValueError as exception:
|
|
raise bumblebee.error.ThemeLoadError("JSON error: {}".format(exception))
|
|
|
|
return result
|
|
|
|
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]
|
|
|
|
mod = widget.get_module()
|
|
if mod and not mod.parameter("is-unittest"):
|
|
value = widget.get_module().parameter("theme.{}".format(name), value)
|
|
|
|
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
|