Files
fedora-dots/.local/state/noctalia/community-templates/blender/blender_theme.py
T
2026-11-04 20:35:46 +01:00

193 lines
7.6 KiB
Python

"""Applies Noctalia's palette to Blender's UI chrome, run headless via `blender --background --python`.
Only structural chrome is themed: panel backgrounds, headers, widget fills/outlines/text.
Workflow-meaningful colors (mesh selection states, node categories, sequencer strip types,
syntax highlighting) are deliberately left at Blender's own defaults, since users rely on those
as functional conventions, not decoration.
"""
import bpy
COLORS = {
"surface": "{{colors.surface.default.hex}}",
"on_surface": "{{colors.on_surface.default.hex}}",
"surface_variant": "{{colors.surface_variant.default.hex}}",
"on_surface_variant": "{{colors.on_surface_variant.default.hex}}",
"surface_container": "{{colors.surface_container.default.hex}}",
"surface_container_low": "{{colors.surface_container_low.default.hex}}",
"surface_container_high": "{{colors.surface_container_high.default.hex}}",
"outline": "{{colors.outline.default.hex}}",
"outline_variant": "{{colors.outline_variant.default.hex}}",
"primary": "{{colors.primary.default.hex}}",
"on_primary": "{{colors.on_primary.default.hex}}",
"primary_container": "{{colors.primary_container.default.hex}}",
"on_primary_container": "{{colors.on_primary_container.default.hex}}",
"secondary": "{{colors.secondary.default.hex}}",
"secondary_container": "{{colors.secondary_container.default.hex}}",
"on_secondary_container": "{{colors.on_secondary_container.default.hex}}",
"tertiary": "{{colors.tertiary.default.hex}}",
"tertiary_container": "{{colors.tertiary_container.default.hex}}",
"on_tertiary_container": "{{colors.on_tertiary_container.default.hex}}",
"error": "{{colors.error.default.hex}}",
"on_error": "{{colors.on_error.default.hex}}",
"error_container": "{{colors.error_container.default.hex}}",
}
def hex_to_rgb(hex_str):
hex_str = hex_str.lstrip("#")
return tuple(int(hex_str[i : i + 2], 16) / 255.0 for i in (0, 2, 4))
RGB = {name: hex_to_rgb(value) for name, value in COLORS.items()}
def classify(path):
if path.endswith(".space.back"):
return "surface"
if path.endswith(".space.header"):
return "surface_container"
if path.endswith(".space.header_text"):
return "on_surface"
if path.endswith(".space.header_text_hi"):
return "primary"
if path.endswith(".space.text"):
return "on_surface"
if path.endswith(".space.text_hi"):
return "primary"
if path.endswith(".space.title"):
return "on_surface_variant"
if path.endswith(".space.tab_active"):
return "surface_container_high"
if path.endswith(".space.tab_back"):
return "surface_container_low"
if path.endswith(".space.tab_outline"):
return "outline"
if path.endswith(".space.gradients.gradient"):
return "surface"
if path.endswith(".space.gradients.high_gradient"):
return "surface_container"
if path.endswith("row_alternate"):
return "surface_container_low"
if ".wcol_" in path:
suffix = path.rsplit(".", 1)[-1]
wcol_map = {
"inner": "surface_container",
"inner_sel": "primary_container",
"inner_key": "tertiary_container",
"inner_key_sel": "tertiary_container",
"inner_anim": "secondary_container",
"inner_anim_sel": "secondary_container",
"inner_driven": "tertiary",
"inner_driven_sel": "tertiary",
"inner_changed": "primary",
"inner_changed_sel": "primary",
"inner_overridden": "error_container",
"inner_overridden_sel": "error_container",
"outline": "outline",
"outline_sel": "primary",
"item": "on_surface_variant",
"text": "on_surface",
"text_sel": "on_primary",
"error": "error",
"warning": "tertiary",
"success": "secondary",
"info": "primary",
}
if suffix in wcol_map:
return wcol_map[suffix]
if path.startswith("regions."):
if path.endswith(".back") or path.endswith(".header_back"):
return "surface_container"
if path.endswith(".tab_back"):
return "surface_container_low"
if path.endswith(".text"):
return "on_surface"
if path.endswith(".text_selected"):
return "primary"
direct = {
"text_editor.line_numbers": "on_surface_variant",
"text_editor.line_numbers_background": "surface_container_low",
"text_editor.cursor": "primary",
"text_editor.selected_text": "primary_container",
"image_editor.metadatabg": "surface_container",
"image_editor.metadatatext": "on_surface",
"sequence_editor.metadatabg": "surface_container",
"sequence_editor.metadatatext": "on_surface",
"sequence_editor.preview_back": "surface",
"clip_editor.metadatabg": "surface_container",
"clip_editor.metadatatext": "on_surface",
"node_editor.node_outline": "outline",
"user_interface.widget_text_cursor": "primary",
"user_interface.editor_border": "outline",
"user_interface.editor_outline": "outline_variant",
"user_interface.editor_outline_active": "primary",
"user_interface.panel_header": "surface_container",
"user_interface.panel_back": "surface_container_low",
"user_interface.panel_outline": "outline",
"user_interface.panel_title": "on_surface",
"user_interface.panel_text": "on_surface",
"user_interface.panel_active": "primary",
"outliner.selected_highlight": "primary_container",
"outliner.active": "primary",
"outliner.selected_object": "on_primary_container",
"outliner.active_object": "on_primary",
"outliner.match": "tertiary_container",
"info.info_error_text": "error",
"info.info_warning_text": "tertiary",
"info.info_info_text": "on_surface",
"info.info_property_text": "secondary",
"info.info_operator_text": "primary",
"info.info_debug_text": "outline",
"info.info_selected": "primary_container",
"info.info_selected_text": "on_primary_container",
"console.line_output": "on_surface",
"console.line_input": "primary",
"console.line_info": "secondary",
"console.line_error": "error",
"console.select": "primary_container",
"console.cursor": "primary",
"file_browser.selected_file": "primary_container",
"preferences.match": "tertiary_container",
"properties.match": "tertiary_container",
}
if path in direct:
return direct[path]
return None
def walk_and_apply(obj, path="", depth=0):
if depth > 4:
return 0
applied = 0
for prop in obj.bl_rna.properties:
if prop.identifier == "rna_type":
continue
try:
val = getattr(obj, prop.identifier)
except Exception:
continue
full = f"{path}.{prop.identifier}" if path else prop.identifier
if prop.type == "POINTER" and val is not None:
applied += walk_and_apply(val, full, depth + 1)
elif prop.type == "FLOAT" and getattr(prop, "array_length", 0) in (3, 4):
role = classify(full)
if role is None:
continue
rgb = RGB[role]
current = list(val)
current[0], current[1], current[2] = rgb
setattr(obj, prop.identifier, current)
applied += 1
return applied
theme = bpy.context.preferences.themes[0]
count = walk_and_apply(theme)
print(f"noctalia-theme: applied {count} properties")
bpy.ops.wm.save_userpref()