src/recolor-svgs.py 5.2 K raw
1
#!/usr/bin/env python3
2
"""Recolour the SVG sources to the Darkmatter palette.
3
4
Two jobs, because the two SVG sets are quite different:
5
6
xfwm4/assets/*.svg are clean and in sync with their PNGs. Upstream draws the
7
window buttons as macOS-style traffic lights -- a coloured disc on a dark rim,
8
with the glyph appearing only on hover. Darkmatter keeps that geometry but
9
drops the hues: the disc is grey at rest and takes the accent on hover. Which
10
colour holds the disc depends on the button's state, so the map is per file.
11
The same source colour also serves two roles at different opacities -- a wash
12
disc at 0.1 and a dot glyph at 0.75 -- so fills are matched together with the
13
fill-opacity beside them.
14
15
assets/*.svg are only build sources; GTK reads the PNGs. Several of them still
16
carry elementary's blues, never re-rendered when Nordic forked, so they are
17
already out of step with their own PNGs. Those get exact-match substitution
18
only: a colour the palette does not name is left exactly as it was rather than
19
guessed at.
20
"""
21
22
import glob
23
import os
24
import re
25
import sys
26
27
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "lib"))
28
29
import darkmatter as dm
30
31
# --- xfwm4 -----------------------------------------------------------------
32
33
FRAME = {
34
    "#353c4a": dm.DARK,      # titlebar and frame background
35
    "#232831": dm.SHADOW,    # rim under the disc
36
    "#231d2b": dm.BG,        # glyph drawn on the disc
37
}
38
39
# The disc colour by button state. Upstream spends a different hue on each
40
# button; Darkmatter spends brightness instead.
41
DISC = {
42
    "active":   dm.DIM,
43
    "inactive": dm.BORDER_HI,
44
    "prelight": dm.SUBTLE,
45
    "pressed":  dm.FG,
46
}
47
# ...except close, which is the one button worth the accent.
48
DISC_CLOSE = {
49
    "active":   dm.DIM,
50
    "inactive": dm.BORDER_HI,
51
    "prelight": dm.ORANGE,
52
    "pressed":  dm.ORANGE_D1,
53
}
54
55
# Source colours that carry the disc, whatever their hue upstream.
56
DISC_SRC = {"#bf616a", "#ebcb8b", "#a3be8c", "#8fbcbb", "#3b4252"}
57
58
# #eceff1 is a wash at low opacity and a dot glyph at high opacity.
59
WASH_MAX_OPACITY = 0.5
60
61
62
def xfwm_map(path):
63
    name = os.path.basename(path)[:-4]
64
    state = name.rsplit("-", 1)[-1]
65
    if state == "shaded":                       # e.g. title-1-active-shaded
66
        state = name.split("-")[-2]
67
    state = state if state in DISC else "active"
68
    disc = (DISC_CLOSE if name.startswith("close") else DISC)[state]
69
70
    def sub(m):
71
        style = m.group(1)
72
        fill = re.search(r"(?<![-\w])fill:(#[0-9a-fA-F]{6})", style)
73
        if not fill:
74
            return m.group(0)
75
        color = fill.group(1).lower()
76
        opac = re.search(r"fill-opacity:([0-9.]+)", style)
77
        opac = float(opac.group(1)) if opac else 1.0
78
79
        if color in FRAME:
80
            new_color = FRAME[color]
81
        elif color == "#eceff1":
82
            new_color = dm.FG if opac <= WASH_MAX_OPACITY else disc
83
        elif color in DISC_SRC:
84
            new_color = disc
85
        else:
86
            return m.group(0)                   # #ffffff highlights, etc.
87
        s, e = fill.span(1)
88
        return 'style="%s"' % (style[:s] + new_color + style[e:])
89
90
    text = open(path).read()
91
    out = re.sub(r'style="([^"]*)"', sub, text)
92
    if out != text:
93
        open(path, "w").write(out)
94
        return True
95
    return False
96
97
98
# --- gtk assets ------------------------------------------------------------
99
100
EXACT = {
101
    "#353c4a": dm.DARK,
102
    "#232831": dm.BG,
103
    "#3b4252": dm.BASE,
104
    "#5d6a83": "#4a494b",
105
    "#8fbcbb": dm.ORANGE,
106
    "#ebcb8b": dm.CREAM,
107
    "#bf616a": dm.RED,
108
    "#a3be8c": dm.TEAL,
109
}
110
CONTROL = {
111
    "close":              dm.DIM,
112
    "close_prelight":     dm.ORANGE,
113
    "close_pressed":      dm.ORANGE_D1,
114
    "close_unfocused":    dm.BORDER_HI,
115
    "maximize":           dm.DIM,
116
    "maximize_prelight":  dm.SUBTLE,
117
    "maximize_pressed":   dm.FG,
118
    "maximize_unfocused": dm.BORDER_HI,
119
    "min":                dm.DIM,
120
    "min_prelight":       dm.SUBTLE,
121
    "min_pressed":        dm.FG,
122
    "min_unfocused":      dm.BORDER_HI,
123
}
124
CONTROL_DISC = {
125
    "close": "#bf616a", "close_prelight": "#bf616a", "close_pressed": "#d52735",
126
    "close_unfocused": "#3b4252",
127
    "maximize": "#a3be8c", "maximize_prelight": "#a3be8c",
128
    "maximize_pressed": "#13c11e", "maximize_unfocused": "#d4d4d4",
129
    "min": "#ebcb8b", "min_prelight": "#ebcb8b",
130
    "min_pressed": "#fac536", "min_unfocused": "#d4d4d4",
131
}
132
CONTROL_RIM = {
133
    "#232831": dm.SHADOW, "#9f1d2b": dm.SHADOW, "#975914": dm.SHADOW,
134
    "#0b7407": dm.SHADOW, "#b4b4b4": dm.SHADOW,
135
}
136
137
138
def asset_map(path):
139
    stem = os.path.basename(path)[:-4].replace("@2", "")
140
    table = dict(EXACT)
141
    if stem in CONTROL:
142
        table.update(CONTROL_RIM)
143
        table[CONTROL_DISC[stem]] = CONTROL[stem]
144
    text = open(path).read()
145
    out = re.sub(
146
        r"#[0-9a-fA-F]{6}",
147
        lambda m: table.get(m.group(0).lower(), m.group(0)),
148
        text,
149
    )
150
    if out != text:
151
        open(path, "w").write(out)
152
        return True
153
    return False
154
155
156
def main():
157
    n = sum(xfwm_map(p) for p in sorted(glob.glob("xfwm4/assets/*.svg")))
158
    print("xfwm4:  recoloured %d SVGs" % n)
159
    n = sum(asset_map(p) for p in sorted(glob.glob("assets/*.svg")))
160
    print("assets: recoloured %d SVGs" % n)
161
162
163
if __name__ == "__main__":
164
    main()