| 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() |