Files
xteink-plugins/plugins/lockscreen/patch.py
T

548 lines
21 KiB
Python

#!/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 <HalStorage.h>',
'#pragma once\n#include <HalStorage.h>\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<SettingInfo> systemSettings;',
' std::vector<SettingInfo> systemSettings;\n std::vector<SettingInfo> 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<LockscreenMode>(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<LockscreenActivity>(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<LockscreenMode>(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 = [
"",
" <script>",
" (function() {",
" var _orig = loadSettings;",
" loadSettings = async function() {",
" await _orig.apply(this, arguments);",
" hideInternalRows();",
" injectLockscreenPin();",
" };",
"",
" function djb2Hash(str) {",
" var hash = 5381;",
" for (var i = 0; i < str.length; i++) {",
" hash = ((hash << 5) + hash) ^ str.charCodeAt(i);",
" hash = hash >>> 0;",
" }",
" hash = (hash ^ 0xA3F1C2B4) >>> 0;",
' return ("00000000" + hash.toString(16).toUpperCase()).slice(-8);',
" }",
"",
" function hideInternalRows() {",
' allSettings.forEach(function(s) {',
' if (s.hidden) {',
' var el = document.getElementById("setting-" + s.key);',
' if (el) { var row = el.closest(".setting-row"); if (row) row.style.display = "none"; }',
' }',
" });",
" }",
"",
" function injectLockscreenPin() {",
' var modeEl = document.getElementById("setting-lockscreenMode");',
" if (!modeEl) return;",
' var row = modeEl.closest(".setting-row");',
' if (!row || document.getElementById("lockscreen-pin-row")) return;',
' var pinRow = document.createElement("div");',
' pinRow.className = "setting-row";',
' pinRow.id = "lockscreen-pin-row";',
" pinRow.innerHTML =",
' \'<span class="setting-name">New PIN (4 digits)</span>\' +',
' \'<span class="setting-control">\' +',
' \'<input type="password" id="lockscreenPin" maxlength="4" \' +',
' \'pattern="[0-9]{4}" inputmode="numeric" \' +',
' \'placeholder="Leave blank to keep current" \' +',
' \'style="width:200px;" oninput="markChanged()">\' +',
" '</span>';",
" row.parentNode.insertBefore(pinRow, row.nextSibling);",
" updatePinRowVisibility();",
' modeEl.addEventListener("change", updatePinRowVisibility);',
" }",
"",
" function updatePinRowVisibility() {",
' var modeEl = document.getElementById("setting-lockscreenMode");',
' var pinRow = document.getElementById("lockscreen-pin-row");',
" if (!modeEl || !pinRow) return;",
' pinRow.style.display = parseInt(modeEl.value) === 1 ? "" : "none";',
" }",
"",
" var _origSave = saveSettings;",
" saveSettings = async function() {",
' var pinEl = document.getElementById("lockscreenPin");',
' var modeEl = document.getElementById("setting-lockscreenMode");',
" if (pinEl && modeEl) {",
" var pin = pinEl.value.trim();",
" if (parseInt(modeEl.value) === 0) {",
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
' if (s) { s.value = ""; originalValues["lockscreenPinHash"] = "CLEAR"; }',
" } else if (pin.length === 4 && /^[0-9]{4}$/.test(pin)) {",
" var hash = djb2Hash(pin);",
' var s = allSettings.find(function(x) { return x.key === "lockscreenPinHash"; });',
' if (s) { s.value = hash; originalValues["lockscreenPinHash"] = "CHANGED"; }',
' pinEl.value = "";',
" } else if (pin.length > 0) {",
' document.getElementById("message").textContent = "PIN must be exactly 4 digits.";',
' document.getElementById("message").className = "message error";',
' document.getElementById("message").style.display = "block";',
' setTimeout(function() { document.getElementById("message").style.display = "none"; }, 4000);',
" return;",
" }",
" }",
" await _origSave.apply(this, arguments);",
" };",
" })();",
" </script>",
"",
]
js = "\n".join(lines)
content = content.replace("</body>", js + "</body>")
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 <path-to-crosspoint-repo>")
patch(sys.argv[1])