Skip to content

Instantly share code, notes, and snippets.

@mzpqnxow
Last active August 7, 2026 13:05
Show Gist options
  • Select an option

  • Save mzpqnxow/8d3d937e5ec61d9bbd23292c9e5cd432 to your computer and use it in GitHub Desktop.

Select an option

Save mzpqnxow/8d3d937e5ec61d9bbd23292c9e5cd432 to your computer and use it in GitHub Desktop.
Create Breeze color schemes for Titlebar color window rules
#!/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