#!/usr/bin/env python3 """ init_user.py ============ Initialize a new user's KiCad environment with UM customizations. This script: 1. Creates ODBC System DSN for SQLite database (Windows only) 2. Copies theme files from UM_KICAD/lib/themes to the user's KiCad config directory 3. Updates KiCad preferences to use the UM theme 4. Sets other UM-specific preferences Usage: python3 init_user.py """ import os import sys import json import shutil from pathlib import Path # --------------------------------------------------------------------------- # KiCad v9 config location # --------------------------------------------------------------------------- def kicad9_config_dir() -> Path: home = Path.home() if sys.platform.startswith("win"): appdata = os.environ.get("APPDATA") if not appdata: raise RuntimeError("APPDATA not set") return Path(appdata) / "kicad" / "9.0" if sys.platform == "darwin": return home / "Library" / "Preferences" / "kicad" / "9.0" xdg = os.environ.get("XDG_CONFIG_HOME") if xdg: return Path(xdg) / "kicad" / "9.0" return home / ".config" / "kicad" / "9.0" def get_themes_dir() -> Path: """Get the user's KiCad themes directory""" return kicad9_config_dir() / "colors" def get_kicad_common_json() -> Path: """Get the path to kicad_common.json (main preferences file)""" return kicad9_config_dir() / "kicad_common.json" # --------------------------------------------------------------------------- # ODBC DSN Creation (Windows only) # --------------------------------------------------------------------------- def create_odbc_dsn(um_root: Path) -> bool: """ Create a System DSN for SQLite ODBC connection on Windows. Args: um_root: Path to UM_KICAD root directory Returns: True if DSN was created or already exists, False on error """ if not sys.platform.startswith("win"): print(" ODBC DSN creation is only supported on Windows") return True # Not an error, just not applicable try: import winreg except ImportError: print(" Warning: winreg module not available, skipping ODBC DSN creation") return False # Database path db_path = um_root / "database" / "parts.sqlite" if not db_path.exists(): print(f" Warning: Database not found at {db_path}") return False dsn_name = "UM_2KiCad_Parts2" driver_name = "SQLite3 ODBC Driver" try: # Check if DSN already exists try: key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ODBC\ODBC.INI\UM_KiCad_Parts", 0, winreg.KEY_READ ) winreg.CloseKey(key) print(f" ODBC DSN '{dsn_name}' already exists") return True except FileNotFoundError: pass # DSN doesn't exist, we'll create it # Create the DSN # First, add to ODBC Data Sources list key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources", 0, winreg.KEY_WRITE ) winreg.SetValueEx(key, dsn_name, 0, winreg.REG_SZ, driver_name) winreg.CloseKey(key) # Create the DSN configuration key key = winreg.CreateKey( winreg.HKEY_LOCAL_MACHINE, rf"SOFTWARE\ODBC\ODBC.INI\{dsn_name}" ) # Set DSN configuration values winreg.SetValueEx(key, "Driver", 0, winreg.REG_SZ, driver_name) winreg.SetValueEx(key, "Database", 0, winreg.REG_SZ, str(db_path)) winreg.SetValueEx(key, "Description", 0, winreg.REG_SZ, "UM KiCad Parts Database") winreg.CloseKey(key) print(f" Created ODBC System DSN '{dsn_name}'") print(f" Database: {db_path}") return True except PermissionError: print(" ERROR: Administrator privileges required to create System DSN") print(" Please run this script as Administrator, or create the DSN manually:") print(f" DSN Name: {dsn_name}") print(f" Driver: {driver_name}") print(f" Database: {db_path}") return False except Exception as e: print(f" Warning: Failed to create ODBC DSN: {e}") print(f" You may need to create it manually:") print(f" DSN Name: {dsn_name}") print(f" Driver: {driver_name}") print(f" Database: {db_path}") return False # --------------------------------------------------------------------------- # Theme installation # --------------------------------------------------------------------------- def install_themes(um_root: Path) -> list[str]: """ Copy all theme files from UM_KICAD/lib/themes to user's KiCad config. Returns: List of installed theme names """ source_themes_dir = um_root / "lib" / "themes" if not source_themes_dir.exists(): print(f"Warning: Themes directory not found at {source_themes_dir}") return [] dest_themes_dir = get_themes_dir() dest_themes_dir.mkdir(parents=True, exist_ok=True) installed = [] theme_files = list(source_themes_dir.glob("*.json")) if not theme_files: print("Warning: No theme files found") return [] for theme_file in theme_files: dest_file = dest_themes_dir / theme_file.name shutil.copy2(theme_file, dest_file) theme_name = theme_file.stem installed.append(theme_name) print(f" Installed theme: {theme_name}") return installed # --------------------------------------------------------------------------- # Preferences configuration # --------------------------------------------------------------------------- def set_theme_preference(theme_name: str): """ Update kicad_common.json and user.json to use the specified theme. Args: theme_name: Name of the theme (without .json extension) """ # 1. Copy the theme to user.json (this is the active theme file) themes_dir = get_themes_dir() source_theme = themes_dir / f"{theme_name}.json" user_theme = themes_dir / "user.json" if source_theme.exists(): shutil.copy2(source_theme, user_theme) print(f" Activated theme '{theme_name}' by copying to user.json") else: print(f" Warning: Theme file not found: {source_theme}") # 2. Update kicad_common.json to reference the theme config_file = get_kicad_common_json() # Load existing config or create new one if config_file.exists(): with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) print(f" Loaded existing preferences from {config_file}") else: config = {} print(f" Creating new preferences file at {config_file}") # Ensure the appearance section exists if "appearance" not in config: config["appearance"] = {} # Set the theme for all applications config["appearance"]["color_theme"] = theme_name # Also set it for specific editors for editor in ["pcb_editor", "schematic_editor", "gerbview", "3d_viewer"]: if editor not in config: config[editor] = {} if "appearance" not in config[editor]: config[editor]["appearance"] = {} config[editor]["appearance"]["color_theme"] = theme_name # Backup existing config if config_file.exists(): backup_file = config_file.with_suffix('.json.bak') shutil.copy2(config_file, backup_file) print(f" Backed up existing config to {backup_file.name}") # Write updated config config_file.parent.mkdir(parents=True, exist_ok=True) with open(config_file, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2) print(f" Set theme to '{theme_name}' in KiCad preferences") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main() -> int: um_root_env = os.environ.get("UM_KICAD") if not um_root_env: print("ERROR: UM_KICAD environment variable not set") print("Please set UM_KICAD to the root of your UM KiCad repository") return 1 um_root = Path(um_root_env).resolve() if not um_root.exists(): print(f"ERROR: UM_KICAD path does not exist: {um_root}") return 1 print("=" * 60) print("UM KiCad User Initialization") print("=" * 60) print(f"\nUM_KICAD: {um_root}") print(f"KiCad config: {kicad9_config_dir()}") print() # Create ODBC DSN (Windows only) if sys.platform.startswith("win"): print("Creating ODBC System DSN...") create_odbc_dsn(um_root) print() # Install themes print("Installing themes...") installed_themes = install_themes(um_root) if not installed_themes: print("\nNo themes were installed") return 1 print(f"\nInstalled {len(installed_themes)} theme(s)") # Set the first theme as default (typically "UM") default_theme = installed_themes[0] print(f"\nSetting default theme...") set_theme_preference(default_theme) print("\n" + "=" * 60) print("Initialization complete!") print("=" * 60) print("\nNext steps:") print("1. Restart KiCad to see the new theme") print("2. You can change themes in Preferences > Colors") print(f"3. Available themes: {', '.join(installed_themes)}") return 0 if __name__ == "__main__": raise SystemExit(main())