#!/usr/bin/env python3 import os import sys import glob import shutil def read_file(path): with open(path, "r", encoding="utf-8") as f: return f.read() def write_file(path, content): with open(path, "w", encoding="utf-8") as f: f.write(content) def find_first(filename, repo_dir): results = glob.glob(os.path.join(repo_dir, "**", filename), recursive=True) if not results: sys.exit(f"ERROR: Could not locate {filename} in {repo_dir}") return results[0] def copy_plugin_sources(plugin_dir, repo_dir): settings_dest = os.path.join(repo_dir, "src", "activities", "settings") activities_dest = os.path.join(repo_dir, "src", "activities") os.makedirs(settings_dest, exist_ok=True) settings_files = { "LockscreenSettingsPage.h", "LockscreenSettingsPage.cpp", "LockscreenPlugin.h", "LockscreenPlugin.cpp", } for fname in os.listdir(plugin_dir): if not fname.endswith((".h", ".cpp", ".hpp")): continue src = os.path.join(plugin_dir, fname) dst = os.path.join(settings_dest if fname in settings_files else activities_dest, fname) shutil.copy2(src, dst) print(f" ✓ {fname}") def patch_cross_point_settings_h(repo_dir): path = find_first("CrossPointSettings.h", repo_dir) content = read_file(path) if "lockscreenMode" in content: print(" CrossPointSettings.h already patched, skipping.") return content = content.replace( '#pragma once\n#include ', '#pragma once\n#include \n#include "activities/settings/LockscreenPlugin.h"' ) content = content.replace( ' uint8_t imageRendering = IMAGES_DISPLAY;', ' uint8_t imageRendering = IMAGES_DISPLAY;\n' ' uint8_t lockscreenMode = 0;\n' ' char lockscreenPinHash[9] = {};' ) write_file(path, content) print(" CrossPointSettings.h patched.") def patch_settings_list_h(repo_dir): path = find_first("SettingsList.h", repo_dir) content = read_file(path) if "lockscreenMode" in content and "lockscreenPinHash" in content: if '{StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}' in content: content = content.replace( ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT,\n' ' StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n', ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n' ) write_file(path, content) print(" SettingsList.h patched (reduced lockscreenMode to 2 options).") else: print(" SettingsList.h already patched, skipping.") return if "lockscreenMode" in content and "lockscreenPinHash" not in content: content = content.replace( ' "lockscreenMode", StrId::STR_NONE_OPT),\n', ' "lockscreenMode", StrId::STR_NONE_OPT),\n' ' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n' ' sizeof(SETTINGS.lockscreenPinHash),\n' ' "lockscreenPinHash", StrId::STR_NONE_OPT),\n' ) write_file(path, content) print(" SettingsList.h patched (added lockscreenPinHash).") return entry = ( '\n' ' SettingInfo::Enum(StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n' ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n' ' "lockscreenMode", StrId::STR_NONE_OPT),\n' ' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.lockscreenPinHash,\n' ' sizeof(SETTINGS.lockscreenPinHash),\n' ' "lockscreenPinHash", StrId::STR_NONE_OPT),\n' ) content = content.replace( ' };\n return list;\n}\n', entry + ' };\n return list;\n}\n' ) write_file(path, content) print(" SettingsList.h patched.") def patch_settings_h(repo_dir): path = find_first("SettingsActivity.h", repo_dir) content = read_file(path) already_has_plugins_tab = "categoryCount = 5" in content already_has_plugins_vec = "pluginsSettings" in content if already_has_plugins_tab and already_has_plugins_vec: print(" SettingsActivity.h already patched, skipping.") return if not already_has_plugins_tab: content = content.replace( 'static constexpr int categoryCount = 4;', 'static constexpr int categoryCount = 5;' ) if not already_has_plugins_vec: content = content.replace( ' std::vector systemSettings;', ' std::vector systemSettings;\n std::vector pluginsSettings;' ) write_file(path, content) print(" SettingsActivity.h patched.") def patch_settings_cpp(repo_dir): path = find_first("SettingsActivity.cpp", repo_dir) content = read_file(path) if '"LockscreenPlugin.h"' in content and 'lockscreenPinHash' in content and '"lockscreenMode") return "Lockscreen"' in content: print(" SettingsActivity.cpp already patched, skipping.") return changed = False if '"LockscreenPlugin.h"' not in content: content = content.replace( '#include "SettingsList.h"', '#include "SettingsList.h"\n' '#include "activities/settings/LockscreenPlugin.h"\n' '#include "activities/LockscreenActivity.h"' ) changed = True if 'STR_NONE_OPT};' not in content: content = content.replace( 'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};', 'StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n' ' StrId::STR_NONE_OPT};' ) changed = True if 'pluginsSettings.clear()' not in content: result = content.replace( ' systemSettings.clear();\n', ' systemSettings.clear();\n pluginsSettings.clear();\n' ) if 'pluginsSettings.clear()' in result: content = result changed = True lockscreen_push = ( ' pluginsSettings.push_back(SettingInfo::Enum(\n' ' StrId::STR_NONE_OPT, &CrossPointSettings::lockscreenMode,\n' ' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n' ' "lockscreenMode", StrId::STR_NONE_OPT\n' ' ));\n' ) if '"lockscreenMode", StrId::STR_NONE_OPT' not in content: for anchor in [ ' selectedCategoryIndex = 0;\n selectedSettingIndex = 0;', ' selectedCategoryIndex = 0;', ' currentSettings = &displaySettings;', ]: if anchor in content: content = content.replace(anchor, lockscreen_push + anchor, 1) changed = True break if 'case 4:' not in content: content = content.replace( ' case 3:\n' ' currentSettings = &systemSettings;\n' ' break;\n', ' case 3:\n' ' currentSettings = &systemSettings;\n' ' break;\n' ' case 4:\n' ' currentSettings = &pluginsSettings;\n' ' break;\n' ) changed = True if '"Plugins"' not in content: content = content.replace( ' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n' ' }', ' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n' ' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n' ' }' ) changed = True if 'nextCatIdx' not in content: content = content.replace( ' const auto confirmLabel = (selectedSettingIndex == 0)\n' ' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n' ' : tr(STR_TOGGLE);', ' const int nextCatIdx = (selectedCategoryIndex + 1) % categoryCount;\n' ' const char* nextCatLabel = (nextCatIdx == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIdx]);\n' ' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);' ) changed = True if 'selectedCategoryIndex == 4' not in content: content = content.replace( ' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },', ' [&settings, this](int index) -> std::string {\n' ' if (selectedCategoryIndex == 4) {\n' ' const auto& s = settings[index];\n' ' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n' ' }\n' ' return std::string(I18N.get(settings[index].nameId));\n' ' },' ) changed = True elif '"lockscreenMode") return "Lockscreen"' not in content: import re result = re.sub( r'( if \(selectedCategoryIndex == 4\) \{.*?)( \})', lambda m: m.group(1) + ' if (s.key && std::string(s.key) == "lockscreenMode") return "Lockscreen";\n' + m.group(2), content, count=1, flags=re.DOTALL, ) if result != content: content = result changed = True if 'LockscreenPlugin::modeName' not in content: import re lockscreen_value = ( ' if (setting.type == SettingType::ENUM &&\n' ' setting.key && std::string(setting.key) == "lockscreenMode") {\n' ' valueText = LockscreenPlugin::modeName(static_cast(SETTINGS.lockscreenMode));\n' ' } else ' ) result = re.sub( r'( if \(setting\.type == SettingType::(?:ACTION|TOGGLE|ENUM))', lockscreen_value + r'\1', content, count=1, ) if result != content: content = result changed = True if 'lockscreenPinHash' not in content: content = content.replace( ' SETTINGS.saveToFile();\n' '}\n' '\n' 'void SettingsActivity::render', ' if (setting.key && std::string(setting.key) == "lockscreenMode") {\n' ' if (SETTINGS.lockscreenMode == 0) {\n' ' memset(SETTINGS.lockscreenPinHash, 0, sizeof(SETTINGS.lockscreenPinHash));\n' " } else if (SETTINGS.lockscreenPinHash[0] == '\\0') {\n" ' startActivityForResult(\n' ' std::make_unique(renderer, mappedInput, LockscreenActivity::Purpose::CREATE),\n' ' [](const ActivityResult&) { SETTINGS.saveToFile(); });\n' ' return;\n' ' }\n' ' }\n' '\n' ' SETTINGS.saveToFile();\n' '}\n' '\n' 'void SettingsActivity::render' ) changed = True if changed: write_file(path, content) print(" SettingsActivity.cpp patched.") else: print(" SettingsActivity.cpp already patched, skipping.") def patch_web_server(repo_dir): path = find_first("CrossPointWebServer.cpp", repo_dir) content = read_file(path) if 'lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue' in content: print(" CrossPointWebServer.cpp already patched, skipping.") return target = ( ' if (s.key && strcmp(s.key, "lockscreenMode") == 0) {\n' ' doc["name"] = "Lockscreen";\n' ' doc["category"] = "Plugins";\n' ' }\n' ) if target in content: content = content.replace( target, ' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n' ) write_file(path, content) print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).") return skip = ' if (s.key && (strcmp(s.key, "lockscreenMode") == 0 || strcmp(s.key, "lockscreenPinHash") == 0)) continue;\n' # Insert continue right after category line regardless of what follows anchor = ' doc["category"] = I18N.get(s.category);\n' if anchor in content: content = content.replace(anchor, anchor + '\n' + skip, 1) write_file(path, content) print(" CrossPointWebServer.cpp patched (lockscreen hidden from WUI).") def patch_main_cpp(repo_dir): path = find_first("main.cpp", repo_dir) content = read_file(path) already_has_includes = "LockscreenPlugin" in content already_has_block = "LockscreenActivity lockAct" in content if already_has_includes and already_has_block: print(" main.cpp already patched, skipping.") return content = content.replace( '#include "CrossPointSettings.h"', '#include "CrossPointSettings.h"\n' '#include "activities/settings/LockscreenPlugin.h"\n' '#include "activities/LockscreenActivity.h"' ) lockscreen_block = ( '\n' ' {\n' ' const auto lsMode = static_cast(SETTINGS.lockscreenMode);\n' ' const bool hasPinSet = SETTINGS.lockscreenPinHash[0] != \'\\0\';\n' ' const bool shouldLock = hasPinSet && LockscreenPlugin::shouldLock(lsMode);\n' '\n' ' if (shouldLock) {\n' ' LockscreenActivity lockAct(renderer, mappedInputManager, LockscreenActivity::Purpose::UNLOCK);\n' ' lockAct.onEnter();\n' ' lockAct.renderDirect();\n' ' while (!lockAct.isDone()) {\n' ' gpio.update();\n' ' lockAct.loop();\n' ' if (lockAct.needsRender()) {\n' ' lockAct.renderDirect();\n' ' }\n' ' delay(10);\n' ' }\n' ' lockAct.onExit();\n' ' renderer.clearScreen();\n' ' renderer.displayBuffer();\n' ' if (!lockAct.wasSuccessful()) {\n' ' enterDeepSleep();\n' ' return;\n' ' }\n' ' }\n' ' }\n' ) anchor = ' RECENT_BOOKS.loadFromFile();\n\n if (HalSystem::isRebootFromPanic())' if anchor in content: content = content.replace( anchor, ' RECENT_BOOKS.loadFromFile();\n' + lockscreen_block + '\n if (HalSystem::isRebootFromPanic())' ) else: idx = content.find('RECENT_BOOKS') print(' DIAG main.cpp around RECENT_BOOKS:', repr(content[max(0,idx-50):idx+300])) import re m = re.search(r'RECENT_BOOKS\.loadFromFile\(\);.*?\n(\s*\n)*(\s*if)', content, re.DOTALL) if m: actual_anchor = content[m.start():m.end()] content = content.replace(actual_anchor, ' RECENT_BOOKS.loadFromFile();\n' + lockscreen_block + '\n' + content[m.end()-len(m.group(2)):m.end()]) print(' main.cpp lockscreen block inserted via regex.') else: print(' WARNING: could not find RECENT_BOOKS anchor in main.cpp') write_file(path, content) has_block = "LockscreenActivity lockAct" in content has_sleep = "enterDeepSleep" in content if has_block: idx2 = content.find("LockscreenActivity lockAct") print(" DIAG lockscreen block context:", repr(content[max(0,idx2-200):idx2+50])) print(f" main.cpp patched. lockscreen_block={has_block} enterDeepSleep={has_sleep}") def patch_settings_html(repo_dir): path = find_first("SettingsPage.html", repo_dir) content = read_file(path) if "lockscreenPin" in content: print(" SettingsPage.html already patched, skipping.") return lines = [ "", " ", "", ] js = "\n".join(lines) content = content.replace("", js + "") write_file(path, content) print(" SettingsPage.html patched.") def patch(repo_dir: str): plugin_dir = os.path.dirname(os.path.abspath(__file__)) print(" Copying plugin sources...") copy_plugin_sources(plugin_dir, repo_dir) print(" Patching CrossPointSettings.h...") patch_cross_point_settings_h(repo_dir) print(" Patching SettingsList.h...") patch_settings_list_h(repo_dir) print(" Patching SettingsActivity.h...") patch_settings_h(repo_dir) print(" Patching SettingsActivity.cpp...") patch_settings_cpp(repo_dir) print(" Patching CrossPointWebServer.cpp...") patch_web_server(repo_dir) print(" Patching main.cpp...") patch_main_cpp(repo_dir) print(" Patching SettingsPage.html (WUI PIN entry)...") patch_settings_html(repo_dir) if __name__ == "__main__": if len(sys.argv) < 2: sys.exit("Usage: python patch.py ") patch(sys.argv[1])