#!/usr/bin/env python3 from datetime import datetime from tempfile import NamedTemporaryFile import configparser import os import shutil import sys import re from typing import Dict, Tuple BASE = os.path.expanduser("~/.trinity/share/config") FILES = { "kateschemarc": os.path.join(BASE, "kateschemarc"), "katesyntaxhighlightingrc": os.path.join(BASE, "katesyntaxhighlightingrc"), "kwriterc": os.path.join(BASE, "kwriterc"), "katerc": os.path.join(BASE, "katerc"), "konquerorrc": os.path.join(BASE, "konquerorrc"), "arkrc": os.path.join(BASE, "arkrc"), "krusaderrc": os.path.join(BASE, "krusaderrc"), "konsoleschema": os.path.expanduser("~/.trinity/share/apps/konsole/Setuzuna_Timir.schema"), "konsolerc": os.path.join(BASE, "konsolerc"), "konsolepartrc": os.path.join(BASE, "konsolepartrc"), } WRAPPED_FILES = set() SECTION_NAME = "setuzuna - timir" SCHEMA_SECTION_NAME = f"Default Item Styles - Schema {SECTION_NAME}" PYTHON_SECTION_NAME = f"Highlighting Python - Schema {SECTION_NAME}" PALETTE: Dict[str, str] = { # core "WHITE": "ffffff", "FOREGROUND": "dfdedc", "BACKGROUND": "1b1e20", "BRACKET_HL": "000000", "LINE_HL": "2a2e32", "ICON_BAR": "222528", "LINENUM": "b0b2b9", "REGION_BG": "40464d", # marks / syntax "MARK1": "56b6c2", "MARK2": "e06c75", "MARK3": "e5c07b", "MARK4": "bf86ff", "MARK5": "8091a1", "MARK6": "98c379", # selection/tab/wrap "SELECTION": "184880", "TAB_MARKER": "3c4252", # syntax-specific "ALERT": "eb6b6b", "COMMENT": "5c6370", "DATATYPE": "61afef", "STRING": "f9ca7b", "NUMBER": "81c784", "KEYWORD": "c792ea", "FUNCTION": "f3e88d", "DECORATOR": "8f6b32", "SPECIAL": "0057ae", # misc "MID_GREY": "686868", "BRIGHT_RED": "ff5454", "BRIGHT_GRN": "54ff54", "BRIGHT_YLW": "ffff54", "BRIGHT_BLU": "5454ff", "BRIGHT_MAG": "ff54ff", "BRIGHT_CYN": "54ffff", } KATE_DOCUMENT_DEFAULTS = { "Basic Config Flags": "583565346", "Indentation Mode": "0", "Indentation Width": "4", "Tab Width": "4", } KATE_RENDERER_DEFAULTS = { "Schema": SECTION_NAME, "Show Indentation Lines": "true", "Word Wrap Marker": "false", } KATE_VIEW_DEFAULTS = { "Line Numbers": "true", "Icon Bar": "false", "Folding Bar": "true", } globals().update({k: v for k, v in PALETTE.items()}) def hex_to_rgb_tuple(h: str) -> Tuple[int, int, int]: return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) def hex_to_rgb_str(hex_or_key: str, sep: str = ",") -> str: r, g, b = hex_to_rgb_tuple(hex_or_key) return f"{r}{sep}{g}{sep}{b}" def ff_hex(h: str) -> str: return f"ff{h}" def build_kate_schema_entries() -> Dict[str, str]: return { "Color Background": hex_to_rgb_str(BACKGROUND), "Color Highlighted Bracket": hex_to_rgb_str(BRACKET_HL), "Color Highlighted Line": hex_to_rgb_str(LINE_HL), "Color Icon Bar": hex_to_rgb_str(ICON_BAR), "Color Line Number": hex_to_rgb_str(LINENUM), "Color MarkType1": hex_to_rgb_str(MARK1), "Color MarkType2": hex_to_rgb_str(MARK2), "Color MarkType3": hex_to_rgb_str(MARK3), "Color MarkType4": hex_to_rgb_str(MARK4), "Color MarkType5": hex_to_rgb_str(MARK5), "Color MarkType6": hex_to_rgb_str(MARK6), "Color MarkType7": hex_to_rgb_str(MARK2), "Color Selection": hex_to_rgb_str(SELECTION), "Color Tab Marker": hex_to_rgb_str(TAB_MARKER), "Color Word Wrap Marker": hex_to_rgb_str(TAB_MARKER), "Font": "Droid Sans Mono,9,-1,5,50,0,0,0,0,0", } def build_syntax_style_entries() -> Dict[str, str]: entries = { "Alert": [ff_hex(WHITE), ff_hex(ALERT), "1", "", "", "", ff_hex(ALERT), "-", "---"], "Base-N Integer": [ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Character": [ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Comment": [ff_hex(COMMENT), ff_hex(WHITE), "", "1", "", "", "", "-", "---"], "Data Type": [ff_hex(DATATYPE), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Decimal/Value": [ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Error": [ff_hex(WHITE), ff_hex(ALERT), "", "", "", "1", "-", "-", "---"], "Floating Point": [ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Function": [ff_hex(FUNCTION), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Keyword": [ff_hex(KEYWORD), ff_hex(WHITE), "1", "", "", "", "-", "-", "---"], "Normal": [ff_hex(FOREGROUND), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Others": [ff_hex(DATATYPE), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], "Region Marker": [ff_hex(WHITE), ff_hex(REGION_BG), "", "", "", "", ff_hex(REGION_BG), "-", "---"], "String": [ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "-", "-", "---"], } return {k: ",".join(v) for k, v in entries.items()} def build_python_style_entries() -> Dict[str, str]: entries = { "Alerts:Alert": ["9", ff_hex(ALERT), ff_hex(ALERT), "", "", "", "", "", "0", "---"], "Alerts:Normal Text": ["0", ff_hex(FOREGROUND), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Alerts_indent:Normal Text": ["0", ff_hex(FOREGROUND), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Comment": ["8", ff_hex(COMMENT), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Keyword": ["1", ff_hex(KEYWORD), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Number": ["3", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Option OFF": ["9", ff_hex(WHITE), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Option ON": ["9", ff_hex(WHITE), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:String": ["7", ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Value": ["5", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Modelines:Variable": ["11", ff_hex(DATATYPE), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Builtin Function": ["2", ff_hex(FUNCTION), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Command Keyword": ["1", ff_hex(KEYWORD), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Comment": ["8", ff_hex(COMMENT), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Complex": ["9", ff_hex(FOREGROUND), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Decorator": ["9", ff_hex(DECORATOR), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Definition Keyword": ["1", ff_hex(KEYWORD), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Exceptions": ["9", ff_hex(ALERT), ff_hex(ALERT), "1", "", "", "", "", "0", "---"], "Python:Extensions": ["9", ff_hex(DATATYPE), ff_hex(WHITE), "1", "", "", "", "", "0", "---"], "Python:Float": ["5", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Flow Control Keyword": ["1", ff_hex(KEYWORD), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Hex": ["4", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Int": ["3", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Normal Text": ["0", ff_hex(FOREGROUND), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Octal": ["4", ff_hex(NUMBER), ff_hex(WHITE), "", "", "", "", "" ,"0", "---"], "Python:Operator": ["0", ff_hex(DATATYPE), ff_hex(WHITE), "1", "", "", "", "", "0", "---"], "Python:Overloaders": ["9", ff_hex(DATATYPE), ff_hex(WHITE), "1", "", "", "", "", "0", "---"], "Python:Preprocessor": ["6", ff_hex(DATATYPE), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Raw String": ["7", ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:Special Variable": ["9", ff_hex(DATATYPE), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:String": ["7", ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:String Char": ["6", ff_hex(STRING), ff_hex(WHITE), "", "", "", "", "", "0", "---"], "Python:String Substitution": ["9", ff_hex(SPECIAL), ff_hex(WHITE), "", "", "", "", "", "0", "---"], } return {k: ",".join(v) for k, v in entries.items()} DEFAULT_KONSOLE_INDEX_MAP = { 0: (FOREGROUND, 0, 0), 1: (BACKGROUND, 1, 0), 2: (BACKGROUND, 0, 0), 3: (MARK2, 0, 0), 4: (MARK6, 0, 0), 5: (MARK3, 0, 0), 6: (SPECIAL, 0, 0), 7: (MARK4, 0, 0), 8: (MARK1, 0, 0), 9: (LINENUM, 0, 0), 10: (WHITE, 0, 0), 11: (MID_GREY, 1, 0), 12: (MID_GREY, 0, 0), 13: (BRIGHT_RED, 0, 0), 14: (BRIGHT_GRN, 0, 0), 15: (BRIGHT_YLW, 0, 0), 16: (BRIGHT_BLU, 0, 0), 17: (BRIGHT_MAG, 0, 0), 18: (BRIGHT_CYN, 0, 0), 19: (WHITE, 0, 0), } def generate_konsole_block() -> str: lines = [] lines.append("title Setuzuna - Timir") lines.append("transparency 0.75 0 0 0") for i in range(20): key, flag1, flag2 = DEFAULT_KONSOLE_INDEX_MAP[i] rgb = hex_to_rgb_str(key, sep=" ") lines.append(f"color {i:2d} {rgb} {flag1} {flag2}") return "\n".join(lines) def backup_file(path): if not os.path.exists(path): return None dirname = os.path.dirname(path) base = os.path.basename(path) ts = datetime.now().strftime("%Y%m%dT%H%M%S") bak = os.path.join(dirname, f"{base}.bak.{ts}") shutil.copy2(path, bak) return bak def load_config(path): parser = configparser.RawConfigParser(strict=False) parser.optionxform = str if not os.path.exists(path): return parser try: with open(path, "r", encoding="utf-8") as f: parser.read_file(f) return parser except configparser.MissingSectionHeaderError: with open(path, "r", encoding="utf-8") as f: content = f.read() wrapped = "[__root__]\n" + content parser.read_string(wrapped) WRAPPED_FILES.add(path) return parser except Exception: try: parser.read(path, encoding="utf-8") except Exception: return configparser.RawConfigParser(strict=False) return parser def remove_dummy_header(path): try: with open(path, "r", encoding="utf-8") as f: lines = f.readlines() except Exception: return False idx = 0 while idx < len(lines) and lines[idx].strip() == "": idx += 1 while idx < len(lines) and lines[idx].lstrip().startswith(("#", ";")): idx += 1 if idx < len(lines): first = lines[idx].lstrip("\ufeff").strip() if first == "[__root__]": del lines[idx] dirn = os.path.dirname(path) or "." with NamedTemporaryFile("w", encoding="utf-8", delete=False, dir=dirn) as tf: tf.writelines(lines) tmpname = tf.name os.replace(tmpname, path) return True return False def safe_write_config(parser, path): dirn = os.path.dirname(path) or "." os.makedirs(dirn, exist_ok=True) with NamedTemporaryFile("w", encoding="utf-8", delete=False, dir=dirn) as tf: parser.write(tf) tmpname = tf.name os.replace(tmpname, path) if path in WRAPPED_FILES: try: removed = remove_dummy_header(path) finally: WRAPPED_FILES.discard(path) def replace_section_with_entries(path, section_name, entries): parser = load_config(path) removed = False if parser.has_section(section_name): parser.remove_section(section_name) removed = True if not parser.has_section(section_name): parser.add_section(section_name) for k, v in entries.items(): parser.set(section_name, k, v) bak = backup_file(path) safe_write_config(parser, path) return bak, removed def update_multiple_sections(path, sections_entries): parser = load_config(path) prev_values = {} created_sections = {} for section, entries in sections_entries.items(): created = False if not parser.has_section(section): parser.add_section(section) created = True if parser.has_option(section, "Schema"): prev_values[section] = parser.get(section, "Schema") else: prev_values[section] = None for k, v in entries.items(): parser.set(section, k, v) created_sections[section] = created bak = backup_file(path) safe_write_config(parser, path) return bak, created_sections, prev_values def set_kate_defaults(path): sections = { "Kate Document Defaults": KATE_DOCUMENT_DEFAULTS, "Kate Renderer Defaults": KATE_RENDERER_DEFAULTS, "Kate View Defaults": KATE_VIEW_DEFAULTS, } return update_multiple_sections(path, sections) def install_konsolerc(path: str) -> Tuple[str, Dict[str, str], bool]: parser = load_config(path) created = False prev = {} section = "Desktop Entry" if not parser.has_section(section): parser.add_section(section) created = True for key in ("EncodingName", "schema"): prev[key] = parser.get(section, key) if parser.has_option(section, key) else None parser.set(section, "EncodingName", "utf8") parser.set(section, "defaultfont", "Noto Mono,8,-1,5,50,0,0,0,0,0") parser.set(section, "schema", "Setuzuna_Timir.schema") parser.set(section, "history", "0") bak = backup_file(path) safe_write_config(parser, path) return bak, prev, created def install_konsolepartrc(path: str) -> Tuple[str, Dict[str, str], bool]: parser = load_config(path) created = False prev = {} section = "Desktop Entry" if not parser.has_section(section): parser.add_section(section) created = True prev_key = "use_konsole_settings" prev[prev_key] = parser.get(section, prev_key) if parser.has_option(section, prev_key) else None parser.set(section, prev_key, "true") bak = backup_file(path) safe_write_config(parser, path) return bak, prev, created if __name__ == "__main__": changes = [] kate_entries = build_kate_schema_entries() syntax_entries = build_syntax_style_entries() python_entries = build_python_style_entries() konsole_text = generate_konsole_block() bak1, removed1 = replace_section_with_entries(FILES["kateschemarc"], SECTION_NAME, kate_entries) changes.append(("kateschemarc", FILES["kateschemarc"], bak1, removed1)) bak2a, removed2a = replace_section_with_entries(FILES["katesyntaxhighlightingrc"], SCHEMA_SECTION_NAME, syntax_entries) changes.append(("katesyntaxhighlightingrc:Default", FILES["katesyntaxhighlightingrc"], bak2a, removed2a)) bak2b, removed2b = replace_section_with_entries(FILES["katesyntaxhighlightingrc"], PYTHON_SECTION_NAME, python_entries) changes.append(("katesyntaxhighlightingrc:Python", FILES["katesyntaxhighlightingrc"], bak2b, removed2b)) for name in ("kwriterc", "katerc", "konquerorrc", "arkrc", "krusaderrc"): path = FILES[name] bak, created, prev = set_kate_defaults(path) changes.append((name, path, bak, created, prev)) try: konsole_dir = os.path.dirname(FILES["konsoleschema"]) os.makedirs(konsole_dir, exist_ok=True) with open(FILES["konsoleschema"], "w", encoding="utf-8") as f: f.write(konsole_text) konsole_bak = None except Exception as e: konsole_bak = f"ERROR_WRITING:{e}" try: bak_konsolerc, prev_konsolerc, created_konsolerc = install_konsolerc(FILES["konsolerc"]) changes.append(("konsolerc", FILES["konsolerc"], bak_konsolerc, created_konsolerc, prev_konsolerc)) except Exception as e: changes.append(("konsolerc", FILES["konsolerc"], f"ERROR:{e}")) try: bak_konsolepartrc, prev_konsolepartrc, created_konsolepartrc = install_konsolepartrc(FILES["konsolepartrc"]) changes.append(("konsolepartrc", FILES["konsolepartrc"], bak_konsolepartrc, created_konsolepartrc, prev_konsolepartrc)) except Exception as e: changes.append(("konsolepartrc", FILES["konsolepartrc"], f"ERROR:{e}")) print("Update summary:") for item in changes: print(" -", item) print(f"\nKonsole theme written to: {FILES['konsoleschema']} (or error: {konsole_bak})") print("\nDone. Backups were created next to original files (if the files existed).")