feat: add Bookerly font plugin with style selection and SmallerFonts integration

This commit is contained in:
Justin Oros
2026-04-21 18:56:14 -07:00
parent 1dc223cde8
commit 7f704878e9
18 changed files with 1336 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
#include "GitHubSyncActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "GitHubSyncPlugin.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
void GitHubSyncActivity::onEnter() {
Activity::onEnter();
state = SYNCING;
result = GitHubSyncPlugin::SyncResult::OK;
requestUpdate();
}
void GitHubSyncActivity::onExit() { Activity::onExit(); }
void GitHubSyncActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "GitHub Sync");
if (state == SYNCING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Syncing...");
renderer.displayBuffer();
return;
}
const char* line1 = nullptr;
const char* line2 = nullptr;
switch (result) {
case GitHubSyncPlugin::SyncResult::OK:
line1 = "Sync complete.";
break;
case GitHubSyncPlugin::SyncResult::NO_WIFI:
line1 = "Not connected to WiFi.";
line2 = "Connect to WiFi and try again.";
break;
case GitHubSyncPlugin::SyncResult::GIT_ERROR:
line1 = "Sync failed.";
line2 = "Check GitHub URL and PAT in Settings > Plugins.";
break;
}
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - (line2 ? 15 : 0), line1, true,
result == GitHubSyncPlugin::SyncResult::OK ? EpdFontFamily::BOLD
: EpdFontFamily::REGULAR);
if (line2)
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 15, line2);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
void GitHubSyncActivity::doSync() {
result = GitHubSyncPlugin::sync();
state = DONE;
requestUpdate();
}
void GitHubSyncActivity::loop() {
if (state == SYNCING) {
{
RenderLock lock(*this);
}
requestUpdateAndWait();
doSync();
return;
}
if (state == DONE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
}
return;
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "GitHubSyncPlugin.h"
#include "activities/Activity.h"
class GitHubSyncActivity final : public Activity {
public:
explicit GitHubSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("GitHubSync", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override { return true; }
void render(RenderLock&&) override;
private:
enum State { SYNCING, DONE };
State state = SYNCING;
GitHubSyncPlugin::SyncResult result = GitHubSyncPlugin::SyncResult::OK;
void doSync();
};
+246
View File
@@ -0,0 +1,246 @@
#include "GitHubSyncPlugin.h"
#include <HTTPClient.h>
#include <HalStorage.h>
#include <Logging.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <string>
#include <vector>
#include "CrossPointSettings.h"
namespace {
constexpr char GITHUB_API_HOST[] = "api.github.com";
constexpr char TREE_SHA_PATH[] = "/.crosspoint/githubsync.sha";
constexpr size_t CHUNK_SIZE = 4096;
struct RepoInfo {
std::string owner;
std::string repo;
std::string branch;
};
RepoInfo parseRepoUrl(const std::string& url) {
RepoInfo info;
std::string u = url;
for (const char* prefix : {"https://github.com/", "http://github.com/", "github.com/"}) {
if (u.rfind(prefix, 0) == 0) {
u = u.substr(strlen(prefix));
break;
}
}
if (!u.empty() && u.back() == '/') u.pop_back();
if (u.size() > 4 && u.substr(u.size() - 4) == ".git")
u = u.substr(0, u.size() - 4);
auto slash = u.find('/');
if (slash == std::string::npos) return info;
info.owner = u.substr(0, slash);
info.repo = u.substr(slash + 1);
info.branch = "";
return info;
}
std::string loadCachedTreeSha() {
FsFile f;
if (!Storage.openFileForRead("GHS", TREE_SHA_PATH, f)) return {};
char buf[64] = {};
f.read(buf, sizeof(buf) - 1);
f.close();
return std::string(buf);
}
void saveCachedTreeSha(const std::string& sha) {
Storage.mkdir("/.crosspoint");
FsFile f;
if (!Storage.openFileForWrite("GHS", TREE_SHA_PATH, f)) return;
f.write(reinterpret_cast<const uint8_t*>(sha.c_str()), sha.size());
f.close();
}
bool downloadFile(const std::string& rawUrl, const std::string& pat,
const std::string& destPath) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, rawUrl.c_str());
http.addHeader("User-Agent", "CrossPoint-GitHubSync/1.0");
if (!pat.empty()) {
std::string auth = "token " + pat;
http.addHeader("Authorization", auth.c_str());
}
int code = http.GET();
if (code != 200) {
LOG_ERR("GHS", "HTTP %d for %s", code, rawUrl.c_str());
http.end();
return false;
}
FsFile f;
if (!Storage.openFileForWrite("GHS", destPath.c_str(), f)) {
LOG_ERR("GHS", "Cannot open %s for write", destPath.c_str());
http.end();
return false;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buf[CHUNK_SIZE];
int total = http.getSize();
int remaining = total;
while (http.connected() && (remaining > 0 || total == -1)) {
size_t avail = stream->available();
if (avail) {
size_t toRead = std::min(avail, CHUNK_SIZE);
size_t read = stream->readBytes(buf, toRead);
f.write(buf, read);
if (total != -1) remaining -= read;
}
delay(1);
}
f.close();
http.end();
return true;
}
bool fetchTree(RepoInfo& info, const std::string& pat,
std::string& outTreeSha,
std::vector<std::pair<std::string, std::string>>& outFiles) {
const char* branches[] = {"main", "master", nullptr};
for (int i = 0; branches[i] != nullptr; i++) {
if (!info.branch.empty() && info.branch != branches[i]) continue;
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
char url[512];
snprintf(url, sizeof(url),
"https://%s/repos/%s/%s/git/trees/%s?recursive=1",
GITHUB_API_HOST, info.owner.c_str(), info.repo.c_str(), branches[i]);
http.begin(client, url);
http.addHeader("User-Agent", "CrossPoint-GitHubSync/1.0");
http.addHeader("Accept", "application/vnd.github+json");
if (!pat.empty()) {
std::string auth = "token " + pat;
http.addHeader("Authorization", auth.c_str());
}
int code = http.GET();
if (code != 200) {
LOG_ERR("GHS", "Tree fetch HTTP %d for branch %s", code, branches[i]);
http.end();
continue;
}
String body = http.getString();
http.end();
if (body.isEmpty()) {
LOG_ERR("GHS", "Empty response for branch %s", branches[i]);
continue;
}
JsonDocument doc;
if (deserializeJson(doc, body) != DeserializationError::Ok) {
LOG_ERR("GHS", "JSON parse error");
continue;
}
outTreeSha = std::string(doc["sha"] | "");
info.branch = branches[i];
JsonArray tree = doc["tree"].as<JsonArray>();
for (JsonObject item : tree) {
const char* type = item["type"] | "";
const char* path = item["path"] | "";
if (strcmp(type, "blob") != 0) continue;
std::string p(path);
bool wanted = false;
if (p == "sleep.bmp") wanted = true;
else if (p.rfind("sleep/", 0) == 0) wanted = true;
else if (p.size() > 5 && p.substr(p.size() - 5) == ".epub") wanted = true;
if (wanted)
outFiles.push_back({p, std::string(item["sha"] | "")});
}
return true;
}
return false;
}
bool syncFiles(RepoInfo& info, const std::string& pat) {
std::string treeSha;
std::vector<std::pair<std::string, std::string>> files;
if (!fetchTree(info, pat, treeSha, files)) return false;
if (!treeSha.empty() && loadCachedTreeSha() == treeSha) {
LOG_DBG("GHS", "Repo unchanged (tree SHA match), nothing to do.");
return true;
}
for (const auto& [path, sha] : files) {
std::string rawUrl =
"https://raw.githubusercontent.com/" + info.owner + "/" +
info.repo + "/" + info.branch + "/" + path;
std::string dest = "/" + path;
std::string dir = dest.substr(0, dest.rfind('/'));
if (!dir.empty() && dir != "/") Storage.mkdir(dir.c_str());
LOG_DBG("GHS", "Downloading %s", path.c_str());
if (!downloadFile(rawUrl, pat, dest)) {
LOG_ERR("GHS", "Failed: %s", path.c_str());
return false;
}
LOG_DBG("GHS", "OK: %s", path.c_str());
}
saveCachedTreeSha(treeSha);
return true;
}
}
namespace GitHubSyncPlugin {
SyncResult sync() {
if (WiFi.status() != WL_CONNECTED) {
LOG_ERR("GHS", "No WiFi");
return SyncResult::NO_WIFI;
}
const std::string url(SETTINGS.githubSyncUrl);
if (url.empty()) {
LOG_ERR("GHS", "No GitHub URL configured");
return SyncResult::GIT_ERROR;
}
const std::string pat(SETTINGS.githubSyncPat);
RepoInfo info = parseRepoUrl(url);
if (info.owner.empty() || info.repo.empty()) {
LOG_ERR("GHS", "Invalid GitHub URL: %s", url.c_str());
return SyncResult::GIT_ERROR;
}
if (!syncFiles(info, pat)) return SyncResult::GIT_ERROR;
return SyncResult::OK;
}
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <Arduino.h>
namespace GitHubSyncPlugin {
enum class SyncResult {
OK,
NO_WIFI,
GIT_ERROR,
};
SyncResult sync();
}
Binary file not shown.
+417
View File
@@ -0,0 +1,417 @@
#!/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):
dest = os.path.join(repo_dir, "src", "activities", "settings")
os.makedirs(dest, exist_ok=True)
for fname in os.listdir(plugin_dir):
if fname.endswith((".h", ".cpp", ".hpp")):
src = os.path.join(plugin_dir, fname)
dst = os.path.join(dest, fname)
shutil.copy2(src, dst)
print(f" \u2713 {fname}")
def patch_cross_point_settings_h(repo_dir, url="", pat=""):
path = find_first("CrossPointSettings.h", repo_dir)
content = read_file(path)
if "githubSyncUrl" in content:
print(" CrossPointSettings.h already patched, skipping.")
return
safe_url = url.replace('\\', '\\\\').replace('"', '\\"')
safe_pat = pat.replace('\\', '\\\\').replace('"', '\\"')
content = content.replace(
' uint8_t imageRendering = IMAGES_DISPLAY;',
f' uint8_t imageRendering = IMAGES_DISPLAY;\n'
f' char githubSyncUrl[512] = "{safe_url}";\n'
f' char githubSyncPat[256] = "{safe_pat}";'
)
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 "githubSyncUrl" in content:
print(" SettingsList.h already patched, skipping.")
return
content = content.replace(
' };\n return list;\n}\n',
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.githubSyncUrl,\n'
' sizeof(SETTINGS.githubSyncUrl), "githubSyncUrl"),\n'
' SettingInfo::String(StrId::STR_NONE_OPT, SETTINGS.githubSyncPat,\n'
' sizeof(SETTINGS.githubSyncPat), "githubSyncPat")\n'
' .withObfuscated(),\n'
' };\n return list;\n}\n'
)
write_file(path, content)
print(" SettingsList.h patched.")
def patch_setting_action_enum(repo_dir):
path = find_first("SettingsActivity.h", repo_dir)
content = read_file(path)
if "GitHubSync" in content:
print(" SettingAction enum already patched, skipping.")
return
content = content.replace(
' Language,\n',
' GitHubSync,\n Language,\n'
)
write_file(path, content)
print(" SettingAction::GitHubSync added.")
def patch_settings_h(repo_dir):
path = find_first("SettingsActivity.h", repo_dir)
content = read_file(path)
already_has_tab = "categoryCount = 5" in content
already_has_vec = "pluginsSettings" in content
if already_has_tab and already_has_vec:
print(" SettingsActivity.h already patched, skipping.")
return
if not already_has_tab:
content = content.replace(
'static constexpr int categoryCount = 4;',
'static constexpr int categoryCount = 5;'
)
if not already_has_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 "GitHubSyncPlugin" in content:
print(" SettingsActivity.cpp already patched, skipping.")
return
content = content.replace(
'#include "SettingsList.h"',
'#include "SettingsList.h"\n#include "GitHubSyncPlugin.h"\n#include "GitHubSyncActivity.h"'
)
if 'StrId::STR_NONE_OPT};' not in content and 'STR_NONE_OPT' not in content.split('categoryNames')[1].split(';')[0]:
content = content.replace(
'const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,\n'
' StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM};',
'const StrId SettingsActivity::categoryNames[categoryCount] = {StrId::STR_CAT_DISPLAY, StrId::STR_CAT_READER,\n'
' StrId::STR_CAT_CONTROLS, StrId::STR_CAT_SYSTEM,\n'
' StrId::STR_NONE_OPT};'
)
if "pluginsSettings.clear();" not in content:
content = content.replace(
' systemSettings.clear();',
' systemSettings.clear();\n pluginsSettings.clear();'
)
github_push = (
' pluginsSettings.push_back(SettingInfo::Action(\n'
' StrId::STR_NONE_OPT, SettingAction::GitHubSync\n'
' ));\n'
)
hardcover_push = (
' pluginsSettings.push_back(SettingInfo::Action(\n'
' StrId::STR_NONE_OPT, SettingAction::HardcoverSync\n'
' ));\n'
)
if 'SettingAction::GitHubSync' not in content:
anchors = [
' pluginsSettings.push_back(SettingInfo::Enum(\n'
' StrId::STR_NONE_OPT, &CrossPointSettings::smallerFontsMode,\n'
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "smallerFontsMode"\n'
' ));\n',
' pluginsSettings.push_back(SettingInfo::Enum(\n'
' StrId::STR_NONE_OPT, &CrossPointSettings::darkModeState,\n'
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT}, "darkModeState"\n'
' ));\n',
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n',
]
for anchor in anchors:
if anchor in content:
content = content.replace(anchor, anchor + github_push + hardcover_push)
break
if 'case 4:\n currentSettings = &pluginsSettings;' not in content:
content = content.replace(
' case 3:\n currentSettings = &systemSettings;\n break;',
' case 3:\n currentSettings = &systemSettings;\n break;\n'
' case 4:\n currentSettings = &pluginsSettings;\n break;'
)
if '"Plugins"' not in content:
content = content.replace(
' std::vector<TabInfo> tabs;\n'
' tabs.reserve(categoryCount);\n'
' for (int i = 0; i < categoryCount; i++) {\n'
' tabs.push_back({I18N.get(categoryNames[i]), selectedCategoryIndex == i});\n'
' }',
' std::vector<TabInfo> tabs;\n'
' tabs.reserve(categoryCount);\n'
' for (int i = 0; i < categoryCount; i++) {\n'
' const char* tabLabel = (i == 4) ? "Plugins" : I18N.get(categoryNames[i]);\n'
' tabs.push_back({tabLabel, selectedCategoryIndex == i});\n'
' }'
)
if 'nextCatLabel' not in content:
content = content.replace(
' const auto confirmLabel = (selectedSettingIndex == 0)\n'
' ? I18N.get(categoryNames[(selectedCategoryIndex + 1) % categoryCount])\n'
' : tr(STR_TOGGLE);',
' const int nextCatIndex = (selectedCategoryIndex + 1) % categoryCount;\n'
' const char* nextCatLabel = (nextCatIndex == 4) ? "Plugins" : I18N.get(categoryNames[nextCatIndex]);\n'
' const auto confirmLabel = (selectedSettingIndex == 0) ? nextCatLabel : tr(STR_TOGGLE);'
)
github_label_line = (
' if (s.type == SettingType::ACTION &&\n'
' s.action == SettingAction::GitHubSync) return "GitHub Sync";\n'
)
if 'GitHub Sync' not in content:
if 'selectedCategoryIndex == 4' in content:
for first_key_line in [
' if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n',
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n',
]:
if first_key_line in content:
content = content.replace(
first_key_line,
github_label_line + first_key_line
)
break
else:
old_lambda = (
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); }, nullptr, nullptr,'
)
new_lambda = (
' [&settings, this](int index) -> std::string {\n'
' if (selectedCategoryIndex == 4) {\n'
' const auto& s = settings[index];\n'
+ github_label_line +
' if (s.key && std::string(s.key) == "darkModeState") return "Dark Mode";\n'
' if (s.key && std::string(s.key) == "smallerFontsMode") return "Smaller Fonts";\n'
' }\n'
' return std::string(I18N.get(settings[index].nameId));\n'
' }, nullptr, nullptr,'
)
content = content.replace(old_lambda, new_lambda)
if '"No WiFi"' not in content:
content = content.replace(
' if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {',
' if (setting.type == SettingType::ACTION &&\n'
' setting.action == SettingAction::GitHubSync) {\n'
' if (WiFi.status() != WL_CONNECTED) {\n'
' valueText = "No WiFi";\n'
' } else {\n'
' valueText = "Sync";\n'
' }\n'
' } else if (setting.type == SettingType::ACTION &&\n'
' setting.action == SettingAction::HardcoverSync) {\n'
' valueText = "Sync";\n'
' } else if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {'
)
if 'case SettingAction::GitHubSync:' not in content:
content = content.replace(
' case SettingAction::Language:\n',
' case SettingAction::GitHubSync:\n'
' startActivityForResult(std::make_unique<GitHubSyncActivity>(renderer, mappedInput), resultHandler);\n'
' break;\n'
' case SettingAction::Language:\n'
)
write_file(path, content)
print(" SettingsActivity.cpp patched.")
def patch_wifi_include(repo_dir):
path = find_first("SettingsActivity.cpp", repo_dir)
content = read_file(path)
if '#include <WiFi.h>' in content:
print(" WiFi.h already included in SettingsActivity.cpp, skipping.")
return
content = content.replace(
'#include "SettingsList.h"',
'#include "SettingsList.h"\n#include <WiFi.h>'
)
write_file(path, content)
print(" WiFi.h include added to SettingsActivity.cpp.")
def patch_web_server(repo_dir):
path = find_first("CrossPointWebServer.cpp", repo_dir)
content = read_file(path)
if '"githubSyncUrl"' in content or '"GitHub Repo"' in content:
print(" CrossPointWebServer.cpp already patched, skipping.")
return
github_block = (
' if (strcmp(s.key, "githubSyncUrl") == 0) {\n'
' doc["name"] = "GitHub Repo";\n'
' doc["category"] = "Plugins";\n'
' }\n'
' if (strcmp(s.key, "githubSyncPat") == 0) {\n'
' doc["name"] = "GitHub PAT (Optional)";\n'
' doc["category"] = "Plugins";\n'
' }\n'
)
if 'if (s.key) {' in content and 'darkModeState' in content:
content = content.replace(
' if (strcmp(s.key, "darkModeState") == 0) {\n',
github_block +
' if (strcmp(s.key, "darkModeState") == 0) {\n'
)
elif 'if (s.key) {' in content and 'hardcoverApiToken' in content:
content = content.replace(
' if (strcmp(s.key, "hardcoverApiToken") == 0) {\n',
github_block +
' if (strcmp(s.key, "hardcoverApiToken") == 0) {\n'
)
else:
content = content.replace(
' doc["category"] = I18N.get(s.category);\n'
'\n'
' switch (s.type) {',
' doc["category"] = I18N.get(s.category);\n'
'\n'
' if (s.key) {\n'
+ github_block +
' }\n'
'\n'
' switch (s.type) {'
)
write_file(path, content)
print(" CrossPointWebServer.cpp patched.")
def patch(repo_dir: str, yes_all: bool = False):
plugin_dir = os.path.dirname(os.path.abspath(__file__))
cache_file = os.path.expanduser("~/.githubsync")
url = ""
pat = ""
if os.path.exists(cache_file):
with open(cache_file) as f:
for line in f:
if line.startswith("url="):
url = line[4:].strip()
elif line.startswith("pat="):
pat = line[4:].strip()
print(f" Using GitHub config from ~/.githubsync (url: {url}).")
else:
print(" GitHub repository URL is required.")
print(" Example: https://github.com/yourusername/yourrepo")
while not url:
url = input(" GitHub URL: ").strip()
if not url:
print(" URL cannot be empty.")
pat = input(" GitHub PAT (Optional for Private repo): ").strip()
with open(cache_file, "w") as f:
f.write(f"url={url}\n")
f.write(f"pat={pat}\n")
print(f" Config saved to ~/.githubsync for future installs.")
print(" Copying plugin sources...")
copy_plugin_sources(plugin_dir, repo_dir)
print(" Patching CrossPointSettings.h...")
patch_cross_point_settings_h(repo_dir, url, pat)
print(" Patching SettingsList.h...")
patch_settings_list_h(repo_dir)
print(" Patching SettingAction enum...")
patch_setting_action_enum(repo_dir)
print(" Patching SettingsActivity.h...")
patch_settings_h(repo_dir)
print(" Patching SettingsActivity.cpp...")
patch_settings_cpp(repo_dir)
print(" Adding WiFi.h include...")
patch_wifi_include(repo_dir)
print(" Patching CrossPointWebServer.cpp (web UI)...")
patch_web_server(repo_dir)
import glob as _glob
cpp_results = _glob.glob(os.path.join(repo_dir, "**", "SettingsActivity.cpp"), recursive=True)
if cpp_results:
cpp_content = open(cpp_results[0]).read()
checks = [
("label lambda: 'GitHub Sync'", "GitHub Sync" in cpp_content),
("switch case: GitHubSync", "case SettingAction::GitHubSync:" in cpp_content),
("GitHubSyncActivity launched", "GitHubSyncActivity" in cpp_content),
("pluginsSettings.clear()", "pluginsSettings.clear();" in cpp_content),
("case 4 -> pluginsSettings", "case 4:" in cpp_content),
("No WiFi button state", "No WiFi" in cpp_content),
]
all_ok = True
for label, ok in checks:
print(f" [{'PASS' if ok else 'FAIL'}] {label}")
if not ok:
all_ok = False
if not all_ok:
sys.exit("ERROR: One or more GitHub Sync patch checks failed.")
print(" GitHub Sync plugin patch verified OK.")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
patch(sys.argv[1])