b0aa0438af
Captures the known-good state after theme consolidation and consistency reconciliation: - NexusOS GTK/xfwm4/icon theme assets (assets/themes, management/Mint-Y-Nexus) - Tightened right-click menus, visible separators, no menu icons - assets/themes/install-theme.sh: idempotent restore of all wiring (symlinks, xfconf xsettings+xfwm4, GTK 3/4 settings.ini) - .gitignore excludes venv/ollama/models/runtime/db Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
164 lines
5.0 KiB
Python
164 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Build NexusOS GTK2 theme from Mint-Y-Dark-Aqua base.
|
|
|
|
- Copies .rc files + assets/ from /usr/share/themes/Mint-Y-Dark-Aqua/gtk-2.0/
|
|
- Recolors PNGs containing the Aqua accent (#1f9ede) to NexusOS green (#8cc63f)
|
|
- Rewrites the gtk-color-scheme block in gtkrc to NexusOS palette
|
|
- Replaces remaining aqua hex literals in .rc files
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import colorsys
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
THEMES_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(THEMES_ROOT))
|
|
import _palette as P # noqa: E402
|
|
|
|
from PIL import Image # noqa: E402
|
|
|
|
SRC = Path("/usr/share/themes/Mint-Y-Dark-Aqua/gtk-2.0")
|
|
OUT = THEMES_ROOT / "NexusOS" / "gtk-2.0"
|
|
|
|
AQUA_RGB = (0x1F, 0x9E, 0xDE)
|
|
AQUA_HSV = colorsys.rgb_to_hsv(*(c / 255 for c in AQUA_RGB))
|
|
GREEN_HSV = P.hex_to_hsv(P.BRAND_GREEN)
|
|
|
|
HUE_TOLERANCE = 30 / 360
|
|
SAT_FLOOR = 0.18
|
|
|
|
S_SCALE = GREEN_HSV[1] / AQUA_HSV[1]
|
|
V_SCALE = GREEN_HSV[2] / AQUA_HSV[2]
|
|
|
|
|
|
def hue_distance(a: float, b: float) -> float:
|
|
d = abs(a - b)
|
|
return min(d, 1 - d)
|
|
|
|
|
|
def recolor_pixel(r: int, g: int, b: int, a: int) -> tuple[int, int, int, int]:
|
|
if a < 8:
|
|
return (r, g, b, a)
|
|
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
|
|
if s < SAT_FLOOR or hue_distance(h, AQUA_HSV[0]) > HUE_TOLERANCE:
|
|
return (r, g, b, a)
|
|
new_s = min(1.0, s * S_SCALE)
|
|
new_v = min(1.0, v * V_SCALE)
|
|
nr, ng, nb = colorsys.hsv_to_rgb(GREEN_HSV[0], new_s, new_v)
|
|
return (round(nr * 255), round(ng * 255), round(nb * 255), a)
|
|
|
|
|
|
def recolor_png(path: Path) -> int:
|
|
"""Recolor aqua pixels in-place. Returns count of pixels changed."""
|
|
im = Image.open(path).convert("RGBA")
|
|
px = im.load()
|
|
w, h = im.size
|
|
changed = 0
|
|
for y in range(h):
|
|
for x in range(w):
|
|
r, g, b, a = px[x, y]
|
|
new = recolor_pixel(r, g, b, a)
|
|
if new != (r, g, b, a):
|
|
px[x, y] = new
|
|
changed += 1
|
|
if changed:
|
|
im.save(path, optimize=True)
|
|
return changed
|
|
|
|
|
|
# ── gtkrc color-scheme block ──────────────────────────────────────────
|
|
NEW_COLOR_SCHEME = (
|
|
f"base_color:#{P.BASE_BG}\\n"
|
|
f"fg_color:#{P.TEXT_PRIMARY}\\n"
|
|
f"tooltip_fg_color:#{P.TEXT_PRIMARY}\\n"
|
|
f"selected_bg_color:#{P.BRAND_PURPLE_DARK}\\n"
|
|
f"selected_fg_color:#{P.TEXT_ON_SELECTION}\\n"
|
|
f"text_color:#{P.TEXT_PRIMARY}\\n"
|
|
f"bg_color:#{P.SURFACE_BG}\\n"
|
|
f"insensitive_bg_color:#{P.SURFACE_BG}\\n"
|
|
f"insensitive_fg_color:#{P.TEXT_DISABLED}\\n"
|
|
f"notebook_bg:#{P.BASE_BG}\\n"
|
|
f"dark_sidebar_bg:#{P.SURFACE_BG_ALT}\\n"
|
|
f"tooltip_bg_color:#{P.OVERLAY_BG}\\n"
|
|
f"link_color:#{P.BRAND_GREEN_LIGHT}\\n"
|
|
f"menu_bg:#{P.SURFACE_BG}\\n"
|
|
f"menu_separator_color:#{P.BORDER}"
|
|
)
|
|
|
|
|
|
def patch_gtkrc(path: Path) -> None:
|
|
text = path.read_text()
|
|
out_lines = []
|
|
for line in text.splitlines():
|
|
if line.startswith("gtk-color-scheme"):
|
|
out_lines.append(f'gtk-color-scheme = "{NEW_COLOR_SCHEME}"')
|
|
else:
|
|
out_lines.append(line)
|
|
path.write_text("\n".join(out_lines) + "\n")
|
|
|
|
|
|
# ── Remaining hex literals in .rc files ───────────────────────────────
|
|
# Captured from Mint-Y-Dark-Aqua; map source hex → NexusOS replacement.
|
|
RC_HEX_REPLACEMENTS = {
|
|
# accent / focus / selection
|
|
"#1f9ede": f"#{P.BRAND_GREEN}",
|
|
"#1F9EDE": f"#{P.BRAND_GREEN}",
|
|
"#5294E2": f"#{P.BRAND_GREEN_LIGHT}", # link_color fallback if quoted literal
|
|
"#5294e2": f"#{P.BRAND_GREEN_LIGHT}",
|
|
# Mint-Y greys we want to nudge onto our surface family
|
|
"#2b2b2b": f"#{P.SURFACE_BG_ALT}",
|
|
"#383838": f"#{P.SURFACE_BG}",
|
|
"#404040": f"#{P.BASE_BG}",
|
|
"#3e3e3e": f"#{P.SURFACE_BG}",
|
|
"#dadada": f"#{P.TEXT_PRIMARY}",
|
|
"#DADADA": f"#{P.TEXT_PRIMARY}",
|
|
"#d3d3d3": f"#{P.TEXT_PRIMARY}",
|
|
"#D3D3D3": f"#{P.TEXT_PRIMARY}",
|
|
}
|
|
|
|
|
|
def patch_rc(path: Path) -> int:
|
|
text = path.read_text()
|
|
count = 0
|
|
for src, dst in RC_HEX_REPLACEMENTS.items():
|
|
n = text.count(src)
|
|
if n:
|
|
text = text.replace(src, dst)
|
|
count += n
|
|
path.write_text(text)
|
|
return count
|
|
|
|
|
|
def main() -> None:
|
|
if OUT.exists():
|
|
shutil.rmtree(OUT)
|
|
shutil.copytree(SRC, OUT)
|
|
print(f"Copied {SRC} → {OUT}")
|
|
|
|
# Recolor PNGs
|
|
recolored = 0
|
|
for png in sorted((OUT / "assets").glob("*.png")):
|
|
changed = recolor_png(png)
|
|
if changed:
|
|
recolored += 1
|
|
print(f" recolored {png.name}: {changed}px")
|
|
print(f"Recolored {recolored} PNGs")
|
|
|
|
# Patch rc files
|
|
patched_total = 0
|
|
for rc in sorted(OUT.glob("*.rc")):
|
|
n = patch_rc(rc)
|
|
if n:
|
|
print(f" patched {rc.name}: {n} replacements")
|
|
patched_total += n
|
|
gtkrc = OUT / "gtkrc"
|
|
patch_rc(gtkrc)
|
|
patch_gtkrc(gtkrc)
|
|
print(f"Rewrote gtkrc color scheme; total .rc hex replacements: {patched_total}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|