├── 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 |
--------------------------------------------------------------------------------