import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
theme_src = (REPO / "src/tui/render/theme.rs").read_text()
enum_match = re.search(r"pub enum Role \{(.*?)\}", theme_src, re.DOTALL)
assert enum_match, "Role enum not found in theme.rs"
ROLE_MAP = {} for line in enum_match.group(1).strip().split("\n"):
m = re.match(r"\s*(\w+)\s*,\s*//\s*palette::(\w+)", line)
if m:
ROLE_MAP[m.group(2)] = m.group(1)
assert len(ROLE_MAP) == 43, f"Expected 43 roles, got {len(ROLE_MAP)}"
SKIP_FILES = {
"src/tui/render/palette.rs",
"src/tui/render/theme.rs",
"src/tui/render/presets.rs",
"src/tui/render/presets_test.rs",
"src/tui/render/mission_control/theme.rs", }
def import_path_for(file_path: str) -> str:
if file_path.startswith("src/tui/render/mission_control/"):
return "use super::super::theme::{self, Role};"
elif file_path.startswith("src/tui/render/skills_dialog/"):
return "use super::super::theme::{self, Role};"
elif file_path.startswith("src/tui/render/profiles_dialog/"):
return "use super::super::theme::{self, Role};"
elif file_path.startswith("src/tui/render/"):
return "use super::theme::{self, Role};"
else:
return "use crate::tui::render::theme::{self, Role};"
def migrate_file(file_path: str, dry_run: bool = False) -> tuple[int, bool]:
path = REPO / file_path
src = path.read_text()
original = src
replacements = 0
def replacer(m):
nonlocal replacements
const_name = m.group(1)
if const_name in ROLE_MAP:
replacements += 1
return f"theme::role(Role::{ROLE_MAP[const_name]})"
return m.group(0)
src = re.sub(r"palette::(\w+)", replacer, src)
if replacements == 0:
return 0, False
import_line = import_path_for(file_path)
import_added = False
old_import_patterns = [
"use super::palette;\n",
"use super::super::palette;\n",
"use crate::tui::render::palette;\n",
]
for old in old_import_patterns:
src = src.replace(old, "")
src = re.sub(r"pub use crate::tui::render::palette::\{[^}]*\};\n", "", src)
if import_line not in src:
lines = src.split("\n")
last_use_idx = -1
for i, line in enumerate(lines):
if line.startswith("use ") and line.rstrip().endswith(";"):
last_use_idx = i
if last_use_idx >= 0:
lines.insert(last_use_idx + 1, import_line)
else:
insert_idx = 0
in_doc = True
for i, line in enumerate(lines):
stripped = line.strip()
if in_doc and (stripped.startswith("//!") or stripped.startswith("//") or stripped == ""):
insert_idx = i + 1
else:
in_doc = False
if not in_doc and stripped == "};":
insert_idx = i + 1
break
if not in_doc and not stripped.startswith("use ") and stripped != "" and not stripped.startswith("//"):
break
lines.insert(insert_idx, import_line)
src = "\n".join(lines)
import_added = True
if not dry_run and src != original:
path.write_text(src)
return replacements, import_added
def main():
dry_run = "--dry-run" in sys.argv
target = None
for arg in sys.argv[1:]:
if not arg.startswith("--"):
target = arg
files = []
for f in sorted(REPO.glob("src/**/*.rs")):
rel = str(f.relative_to(REPO))
if rel in SKIP_FILES:
continue
content = f.read_text()
if "palette::" in content:
files.append(rel)
if target:
files = [f for f in files if f == target]
if dry_run:
print(f"DRY RUN — {len(files)} files to migrate\n")
total = 0
for f in files:
count, imported = migrate_file(f, dry_run=dry_run)
if count > 0:
prefix = "[DRY] " if dry_run else ""
import_note = " +import" if imported else ""
print(f"{prefix}{count:3d} replacements in {f}{import_note}")
total += count
print(f"\n{'Would migrate' if dry_run else 'Migrated'}: {total} sites across {len(files)} files")
if __name__ == "__main__":
main()