noctalia
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user