├── README.md └── labwc-gtktheme.py /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `labwc-gtktheme.py` - A tool to parse the current [GTK theme], particularly 4 | [colors] and produce an [openbox]/[labwc] theme. 5 | 6 | Set the theme to `GTK` in your `~/.config/labwc/rc.xml` to use it. 7 | 8 | ``` 9 | 10 | GTK 11 | 12 | ``` 13 | 14 | ## TODO 15 | 16 | - [ ] Resolve @labels in @define-color prior to parsing in order to support shade() 17 | - [ ] Support shade() - not that it makes a difference to most themes, but there 18 | are some that look better if we parse it 19 | 20 | [GTK theme]: https://docs.gtk.org/gtk3/css-overview.html 21 | [colors]: https://docs.gtk.org/gtk3/css-overview.html#colors 22 | [openbox]: http://openbox.org/ 23 | [labwc]: https://github.com/labwc/labwc 24 | -------------------------------------------------------------------------------- /labwc-gtktheme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Create labwc theme based on the current Gtk theme 5 | 6 | SPDX-License-Identifier: GPL-2.0-only 7 | 8 | Copyright (C) @Misko_2083 2019 9 | Copyright (C) Johan Malm 2019-2022 10 | """ 11 | 12 | import os 13 | import errno 14 | import argparse 15 | from tokenize import tokenize, NUMBER, NAME, OP 16 | from io import BytesIO 17 | import gi 18 | gi.require_version("Gtk", "3.0") 19 | from gi.repository import Gtk 20 | 21 | def parse(tokens): 22 | """ 23 | Parse css color expression token list and return red/green/blue values 24 | Valid name-tokens include 'rgb' and 'rgba', whereas 'alpha', 'shade' and 25 | 'mix' are ignored. @other-color references are still to be implemented. 26 | """ 27 | nr_colors_to_parse = 0 28 | in_label = False 29 | color = [] 30 | for toknum, tokval, _, _, _ in tokens: 31 | if '@' in tokval: 32 | in_label = True 33 | continue 34 | if toknum == NAME and in_label: 35 | color.clear() 36 | color.append(f"@{tokval}") 37 | return color 38 | if nr_colors_to_parse > 0: 39 | if toknum == OP and tokval in ')': 40 | print("warn: still parsing numbers; did not expect ')'") 41 | if toknum == NUMBER: 42 | color.append(tokval) 43 | nr_colors_to_parse -= 1 44 | continue 45 | if toknum == NAME and tokval in 'rgb': 46 | nr_colors_to_parse = 3 47 | elif toknum == NAME and tokval in 'rgba': 48 | nr_colors_to_parse = 4 49 | return color 50 | 51 | def color_hex(color): 52 | """ return rrggbb color hex from list [r, g, b,...] """ 53 | if not color: 54 | return "None" 55 | elif len(color) < 3: 56 | return f"{color[0]}" 57 | return '{:02x}{:02x}{:02x}'.format(*(int(x) for x in color[:3])) 58 | 59 | def hex_from_expr(line): 60 | """ parse color expression to return hex style rrggbb """ 61 | tokens = tokenize(BytesIO(line.encode('utf-8')).readline) 62 | color = parse(tokens) 63 | return color_hex(color) 64 | 65 | def mkdir_p(path): 66 | try: 67 | os.makedirs(path) 68 | except OSError as exc: 69 | if exc.errno == errno.EEXIST and os.path.isdir(path): 70 | pass 71 | else: 72 | raise 73 | 74 | def add(file, key, color): 75 | if color is None: 76 | print(f"warn: no color for {key}") 77 | return 78 | file.write(f"{key}: #{color}\n") 79 | 80 | def parse_section(lines, name): 81 | theme = {} 82 | inside = False 83 | for line in lines: 84 | if f"{name} {{" in line: 85 | inside = True 86 | continue 87 | if inside: 88 | if "}" in line or "{" in line: 89 | inside = False 90 | continue 91 | if 'color' not in line: 92 | continue 93 | key, value = line.strip().split(":", maxsplit=1) 94 | theme[f'{name}.{key.replace(" ", "")}'] = hex_from_expr(value) 95 | return theme 96 | 97 | def remove_self_referencing_entries(theme): 98 | for key, label in theme.items(): 99 | if f'@{key}' == f'{label}': 100 | # A self-referencing line like @define-color foo @foo is bad 101 | print(f'warn: ignore bad line @define-color {key} {label}') 102 | theme.pop(key) 103 | return remove_self_referencing_entries(theme) 104 | return theme 105 | 106 | def resolve_labels(theme): 107 | for key, label in theme.items(): 108 | if '@' in label: 109 | for tmp, value in theme.items(): 110 | if tmp == label[1:]: 111 | theme[key] = value 112 | return resolve_labels(theme) 113 | return theme 114 | 115 | def main(): 116 | """ main """ 117 | parser = argparse.ArgumentParser(prog="labwc-gtktheme") 118 | parser.add_argument("--css", help="dump css and exit", action='store_true') 119 | parser.add_argument("--colors", help="dump colors and exit", action='store_true') 120 | parser.add_argument("--themename", 121 | help="use this theme rather than autodetecting the used one") 122 | args = parser.parse_args() 123 | 124 | themename = args.themename 125 | if not themename: 126 | gset = Gtk.Settings.get_default() 127 | themename = gset.get_property("gtk-theme-name") 128 | css = Gtk.CssProvider.get_named(themename).to_string() 129 | 130 | if args.css: 131 | print(css) 132 | return 133 | 134 | lines = css.split("\n") 135 | theme = {} 136 | 137 | # Parse @define-color lines using syntax "@define-color " 138 | for line in lines: 139 | if "@define-color" not in line: 140 | continue 141 | x = line.split(" ", maxsplit=2) 142 | theme[x[1]] = hex_from_expr(x[2]) 143 | 144 | # Add the color definitions in the headerbar{} and menu{} sections 145 | theme.update(parse_section(lines, "headerbar")) 146 | theme.update(parse_section(lines, "menu")) 147 | 148 | theme = remove_self_referencing_entries(theme) 149 | theme = resolve_labels(theme) 150 | 151 | # Set fallbacks 152 | # Most themes contain headerbar.border-top-color, but Materia does not 153 | if not 'headerbar.border-top-color' in theme.keys(): 154 | theme[f'headerbar.border-top-color'] = theme["theme_bg_color"] 155 | 156 | if args.colors: 157 | for key, value in theme.items(): 158 | print(f"{key}: {value}") 159 | return 160 | 161 | themename = 'GTK' 162 | themedir = os.getenv("HOME") + "/.local/share/themes/" + themename + "/openbox-3" 163 | mkdir_p(themedir) 164 | themefile = themedir + "/themerc" 165 | print(f"Create theme {themename} at {themedir}") 166 | 167 | with open(themefile, "w") as f: 168 | add(f, "window.active.title.bg.color", theme["theme_bg_color"]) 169 | add(f, "window.inactive.title.bg.color", theme["theme_bg_color"]) 170 | 171 | add(f, "window.active.label.text.color", theme["theme_text_color"]) 172 | add(f, "window.inactive.label.text.color", theme["theme_text_color"]) 173 | 174 | add(f, "window.active.border.color", theme["headerbar.border-top-color"]) 175 | add(f, "window.inactive.border.color", theme["headerbar.border-top-color"]) 176 | 177 | add(f, "window.active.button.unpressed.image.color", theme["theme_fg_color"]) 178 | add(f, "window.inactive.button.unpressed.image.color", theme["theme_fg_color"]) 179 | 180 | add(f, "menu.items.bg.color", theme["menu.background-color"]) 181 | add(f, "menu.items.text.color", theme["theme_fg_color"]) 182 | 183 | add(f, "menu.items.active.bg.color", theme["theme_fg_color"]) 184 | add(f, "menu.items.active.text.color", theme["theme_bg_color"]) 185 | 186 | add(f, "osd.bg.color", theme["theme_bg_color"]) 187 | add(f, "osd.border.color", theme["theme_fg_color"]) 188 | add(f, "osd.label.text.color", theme["theme_fg_color"]) 189 | 190 | # TODO: 191 | # border.width: 1 192 | # padding.height: 3 193 | # menu.overlap.x: 0 194 | # menu.overlap.y: 0 195 | # window.label.text.justify: center 196 | # osd.border.width: 1 197 | 198 | if __name__ == '__main__': 199 | main() 200 | --------------------------------------------------------------------------------