Last active
August 7, 2026 13:05
-
-
Save mzpqnxow/8d3d937e5ec61d9bbd23292c9e5cd432 to your computer and use it in GitHub Desktop.
Create Breeze color schemes for Titlebar color window rules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Generate Breeze color-scheme variants with colored titlebars. | |
| To use, run the script (no arguments), it will create color profiles | |
| in your home directory. | |
| Then, go to Settings->Window Rules->Add Rule. Enter the rules you want | |
| (e.g. window class name, ...) then select add property, select title bar | |
| color, and you should see a bunch of colors available | |
| Each generated scheme is a copy of the system BreezeLight scheme with the | |
| titlebar sections ([WM] and [Colors:Header]) replaced, for use with the | |
| KWin window-rule property "Titlebar color scheme" (decocolor) to mark | |
| windows by origin (VM, container, remote, ...). | |
| Output goes to ~/.local/share/color-schemes/. Idempotent: rerunning | |
| overwrites the generated files. | |
| """ | |
| import configparser | |
| import pathlib | |
| BASE_SCHEME = pathlib.Path("/usr/share/color-schemes/BreezeLight.colors") | |
| OUT_DIR = pathlib.Path.home() / ".local/share/color-schemes" | |
| # Breeze neutrals used for blending and text | |
| BREEZE_INACTIVE_BG = (239, 240, 241) | |
| DARK_TEXT = (35, 38, 41) | |
| LIGHT_TEXT = (255, 255, 255) | |
| # name -> (active titlebar RGB, suggested use) | |
| SCHEMES = { | |
| "Red": ((190, 33, 47), "VMs (waypipe)"), | |
| "Orange": ((222, 119, 24), "containers"), | |
| "Yellow": ((240, 198, 43), "semi-trusted / testing"), | |
| "Green": ((37, 133, 58), "SSH / remote forwarded"), | |
| "Blue": ((30, 98, 178), "work / dedicated project domain"), | |
| "Purple": ((125, 66, 167), "untrusted browser"), | |
| "Gray": ((90, 95, 100), "disposable / other"), | |
| } | |
| def mix(a: tuple, b: tuple, t: float) -> tuple: | |
| """Linear blend of two RGB triples: t=0 gives a, t=1 gives b.""" | |
| return tuple(round(a[i] + (b[i] - a[i]) * t) for i in range(3)) | |
| def text_color(bg: tuple) -> tuple: | |
| """Pick dark or light text by background luminance (sRGB approximation).""" | |
| luminance = 0.2126 * bg[0] + 0.7152 * bg[1] + 0.0722 * bg[2] | |
| return DARK_TEXT if luminance > 140 else LIGHT_TEXT | |
| def rgb(c: tuple) -> str: | |
| return ",".join(str(v) for v in c) | |
| def generate(color_name: str, active_bg: tuple) -> pathlib.Path: | |
| scheme_id = f"Breeze{color_name}Titlebar" | |
| # Unfocused titlebar: muted toward Breeze's neutral inactive gray, so | |
| # windows stay identifiable without shouting. Text softened toward bg. | |
| inactive_bg = mix(active_bg, BREEZE_INACTIVE_BG, 0.55) | |
| active_fg = text_color(active_bg) | |
| inactive_fg = mix(text_color(inactive_bg), inactive_bg, 0.25) | |
| cp = configparser.ConfigParser(interpolation=None) | |
| cp.optionxform = str # preserve key case | |
| cp.read(BASE_SCHEME) | |
| # Legacy titlebar group (fallback path for decoration themes) | |
| wm = cp["WM"] | |
| wm["activeBackground"] = wm["activeBlend"] = rgb(active_bg) | |
| wm["activeForeground"] = rgb(active_fg) | |
| wm["inactiveBackground"] = wm["inactiveBlend"] = rgb(inactive_bg) | |
| wm["inactiveForeground"] = rgb(inactive_fg) | |
| # Header group (preferred by Breeze since Plasma 5.23) | |
| cp["Colors:Header"]["BackgroundNormal"] = rgb(active_bg) | |
| cp["Colors:Header"]["ForegroundNormal"] = rgb(active_fg) | |
| cp["Colors:Header][Inactive"]["BackgroundNormal"] = rgb(inactive_bg) | |
| cp["Colors:Header][Inactive"]["ForegroundNormal"] = rgb(inactive_fg) | |
| cp["General"]["Name"] = f"Breeze {color_name} Titlebar" | |
| cp["General"]["ColorScheme"] = scheme_id | |
| out = OUT_DIR / f"{scheme_id}.colors" | |
| with open(out, "w") as f: | |
| cp.write(f, space_around_delimiters=False) | |
| return out | |
| def main() -> None: | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| for color_name, (active_bg, use) in SCHEMES.items(): | |
| out = generate(color_name, active_bg) | |
| print(f"{out.name:32} {rgb(active_bg):>12} # {use}") | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment