xteink-plugins
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||

|
||||
|
||||
# xteink-plugins
|
||||
|
||||
A plugin system for customizing and extending [CrossPoint Reader](https://github.com/crosspoint-reader/crosspoint-reader) firmware on your xteink device. Plugins are applied as source-level patches before the firmware is compiled and flashed.
|
||||
|
||||
## Plugins
|
||||
|
||||
### Smaller Fonts
|
||||
|
||||
Adds a **Plugins** settings tab to CrossPoint with a "Smaller Fonts" option. When enabled, your chosen reader font is transparently substituted with a smaller variant — no need to change your font preference.
|
||||
|
||||
| Mode | Effect |
|
||||
|------|--------|
|
||||
| **Disabled** | No change (default) |
|
||||
| **Smaller** | Drops the current font size down by one step (e.g. Bookerly 16 → 14) |
|
||||
| **Smallest** | Drops the current font size down by two steps (e.g. Bookerly 16 → 12) |
|
||||
|
||||
Supports Bookerly, Noto Sans, and OpenDyslexic. The plugin also generates and embeds Bookerly at 8pt and 10pt — sizes not included in the stock firmware.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- [PlatformIO](https://platformio.org/) (`pio` on your PATH)
|
||||
- `git`
|
||||
- Your xteink device connected via USB
|
||||
|
||||
## Usage
|
||||
|
||||
From the root of this repository, run:
|
||||
|
||||
```bash
|
||||
python install.py
|
||||
```
|
||||
|
||||
The installer will:
|
||||
|
||||
1. Clone the CrossPoint Reader source repository
|
||||
2. Prompt you to select which plugins to install and apply them as patches
|
||||
3. Build the firmware with PlatformIO
|
||||
4. Auto-detect your device's serial port and flash the firmware
|
||||
|
||||
> **Note:** This script modifies and flashes custom firmware to your device. The author accepts no responsibility for any damage that may occur to your device as a result of using this installer.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
xteink-plugins/
|
||||
├── install.py # Interactive installer: clone → patch → build → flash
|
||||
└── plugins/
|
||||
└── smallerfonts/
|
||||
├── patch.py # Patch script applied to the CrossPoint source
|
||||
├── SmallerFontsPlugin.h/.cpp # Font resolution logic
|
||||
└── SmallerFontsSettingsPage.h/.cpp # Settings UI activity
|
||||
```
|
||||
|
||||
## Adding a Plugin
|
||||
|
||||
1. Create a new directory under `plugins/` with your plugin's name.
|
||||
2. Add a `patch.py` file with a `patch(repo_dir: str)` function. This function receives the absolute path to the cloned CrossPoint repository and should make all necessary modifications.
|
||||
|
||||
The installer will automatically discover and offer to install any directory under `plugins/` that contains a `patch.py`.
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import glob
|
||||
import importlib.util
|
||||
|
||||
REPO_URL = "https://github.com/crosspoint-reader/crosspoint-reader.git"
|
||||
REPO_DIR = "crosspoint-reader"
|
||||
PLUGINS_DIR = "plugins"
|
||||
|
||||
|
||||
def run(cmd, cwd=None, check=True):
|
||||
print(f" $ {cmd}")
|
||||
return subprocess.run(cmd, shell=True, cwd=cwd, check=check)
|
||||
|
||||
|
||||
def clone_repo():
|
||||
print("[1/3] Cloning CrossPoint repository...")
|
||||
if os.path.exists(REPO_DIR):
|
||||
print(f" Removing existing '{REPO_DIR}'...")
|
||||
shutil.rmtree(REPO_DIR)
|
||||
run(f"git clone --recursive {REPO_URL} {REPO_DIR}")
|
||||
|
||||
|
||||
def run_plugin_patch(plugin_dir: str) -> bool:
|
||||
patch_path = os.path.join(plugin_dir, "patch.py")
|
||||
if not os.path.exists(patch_path):
|
||||
print(f" WARNING: No patch.py found in {plugin_dir}, skipping.")
|
||||
return False
|
||||
|
||||
spec = importlib.util.spec_from_file_location("patch", patch_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
if hasattr(module, "patch"):
|
||||
module.patch(os.path.abspath(REPO_DIR))
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ERROR while running patch.py for {plugin_dir}: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
def apply_plugins():
|
||||
print("[2/3] Applying plugins...")
|
||||
|
||||
if not os.path.exists(PLUGINS_DIR):
|
||||
print(f" No '{PLUGINS_DIR}' directory found — skipping plugin step.")
|
||||
return
|
||||
|
||||
plugin_dirs = sorted(
|
||||
d for d in os.listdir(PLUGINS_DIR)
|
||||
if os.path.isdir(os.path.join(PLUGINS_DIR, d))
|
||||
)
|
||||
|
||||
if not plugin_dirs:
|
||||
print(" No plugin directories found.")
|
||||
return
|
||||
|
||||
for plugin_name in plugin_dirs:
|
||||
plugin_path = os.path.join(PLUGINS_DIR, plugin_name)
|
||||
|
||||
print(f"\n Plugin: {plugin_name}")
|
||||
answer = input(f" Install '{plugin_name}'? [Y/n]: ").strip().lower()
|
||||
|
||||
if answer in ("", "y", "yes"):
|
||||
print(f" Installing {plugin_name}...")
|
||||
success = run_plugin_patch(plugin_path)
|
||||
if success:
|
||||
print(f" ✓ {plugin_name} installed.")
|
||||
else:
|
||||
print(f" ✗ {plugin_name} failed or was skipped.")
|
||||
else:
|
||||
print(f" Skipping {plugin_name}.")
|
||||
|
||||
|
||||
def list_likely_serial_ports():
|
||||
patterns = [
|
||||
"/dev/cu.usb*",
|
||||
"/dev/tty.usb*",
|
||||
"/dev/cu.wchusb*",
|
||||
"/dev/tty.wchusb*",
|
||||
"/dev/cu.SLAB*",
|
||||
"/dev/tty.SLAB*",
|
||||
"/dev/ttyACM*",
|
||||
"/dev/ttyUSB*",
|
||||
]
|
||||
ports = []
|
||||
for pattern in patterns:
|
||||
ports.extend(glob.glob(pattern))
|
||||
return sorted(set(ports))
|
||||
|
||||
|
||||
def detect_serial_ports():
|
||||
port_map: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
import serial.tools.list_ports
|
||||
|
||||
usb_keywords = {
|
||||
"usb", "cp210", "ftdi", "ch340", "ch9102", "esp",
|
||||
"serial", "uart", "prolific", "silabs",
|
||||
}
|
||||
skip_keywords = {"bluetooth", "bt-", "btle", "airpods", "handsfree"}
|
||||
|
||||
for port in serial.tools.list_ports.comports():
|
||||
combined = (port.device + " " + (port.description or "") +
|
||||
" " + (port.hwid or "")).lower()
|
||||
|
||||
if any(s in combined for s in skip_keywords):
|
||||
continue
|
||||
if any(k in combined for k in usb_keywords):
|
||||
port_map[port.device] = port.description or "USB Serial Device"
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
for device in list_likely_serial_ports():
|
||||
if device not in port_map:
|
||||
port_map[device] = "Detected via glob pattern"
|
||||
|
||||
return sorted(port_map.items())
|
||||
|
||||
|
||||
def prompt_for_upload_port():
|
||||
print("\n[3/3] Select serial port for flashing...")
|
||||
|
||||
max_retries = 3
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
print(f"\n Attempt {attempt}/{max_retries}")
|
||||
print(" Ensure your xteink device is connected via USB and awake.")
|
||||
input(" Press Enter when ready... ")
|
||||
|
||||
ports = detect_serial_ports()
|
||||
|
||||
if ports:
|
||||
cu_ports = [(d, desc) for d, desc in ports if d.startswith("/dev/cu.")]
|
||||
recommended = cu_ports[0][0] if cu_ports else ports[0][0]
|
||||
|
||||
print(f"\n Found {len(ports)} serial port(s):")
|
||||
for i, (device, desc) in enumerate(ports, 1):
|
||||
tag = " ← recommended" if device == recommended else ""
|
||||
print(f" {i}. {device} — {desc}{tag}")
|
||||
|
||||
choice = input(f"\n Press ENTER to use {recommended}, or enter 1-{len(ports)}: ").strip()
|
||||
|
||||
if not choice:
|
||||
print(f" Selected: {recommended}")
|
||||
return recommended
|
||||
|
||||
if choice.isdigit():
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(ports):
|
||||
print(f" Selected: {ports[idx][0]}")
|
||||
return ports[idx][0]
|
||||
|
||||
print(" Invalid choice, please try again.")
|
||||
|
||||
else:
|
||||
print(" No USB serial ports detected.")
|
||||
if attempt < max_retries:
|
||||
print(" Make sure the device is connected, powered on, and awake.")
|
||||
|
||||
print("\n Could not auto-detect a port.")
|
||||
print(" Common names: macOS: /dev/cu.usb* Linux: /dev/ttyUSB* Windows: COMx")
|
||||
|
||||
while True:
|
||||
port = input("\n Enter port manually (or 'quit' to exit): ").strip()
|
||||
if port.lower() == "quit":
|
||||
sys.exit(1)
|
||||
if port:
|
||||
return port
|
||||
print(" Please enter a valid port name.")
|
||||
|
||||
|
||||
def build_and_flash():
|
||||
print("\nBuilding firmware with PlatformIO...")
|
||||
run("pio run --environment default", cwd=REPO_DIR)
|
||||
|
||||
port = prompt_for_upload_port()
|
||||
|
||||
print(f"\n Flashing to {port}...")
|
||||
run(
|
||||
f"pio run --target upload --environment default --upload-port {port}",
|
||||
cwd=REPO_DIR,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(" xteink Plugin Installer")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print(" This script modifies and flashes custom firmware to your")
|
||||
print(" device. The author of this script (Justin Oros) accepts")
|
||||
print(" no responsibility for any damage that may occur to your")
|
||||
print(" device as a result of using this installer.")
|
||||
print()
|
||||
answer = input(" Do you wish to proceed? [Y/n]: ").strip().lower()
|
||||
if answer not in ("", "y", "yes"):
|
||||
print(" Aborted.")
|
||||
sys.exit(0)
|
||||
print()
|
||||
clone_repo()
|
||||
apply_plugins()
|
||||
build_and_flash()
|
||||
print("\nDone.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
#include "SmallerFontsPlugin.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace SmallerFontsPlugin {
|
||||
|
||||
const char* modeName(SmallerFontsMode mode) {
|
||||
switch (mode) {
|
||||
case SmallerFontsMode::SMALLER: return "Smaller";
|
||||
case SmallerFontsMode::SMALLEST: return "Smallest";
|
||||
default: return "Disabled";
|
||||
}
|
||||
}
|
||||
|
||||
int resolveReaderFontId(int baseFontId, SmallerFontsMode mode) {
|
||||
if (mode == SmallerFontsMode::MODE_OFF) return baseFontId;
|
||||
|
||||
if (baseFontId == BOOKERLY_12_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_10_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_8_FONT_ID;
|
||||
}
|
||||
if (baseFontId == BOOKERLY_14_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_12_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_10_FONT_ID;
|
||||
}
|
||||
if (baseFontId == BOOKERLY_16_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_14_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_12_FONT_ID;
|
||||
}
|
||||
if (baseFontId == BOOKERLY_18_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return BOOKERLY_16_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return BOOKERLY_14_FONT_ID;
|
||||
}
|
||||
|
||||
if (baseFontId == NOTOSANS_12_FONT_ID) return NOTOSANS_12_FONT_ID;
|
||||
if (baseFontId == NOTOSANS_14_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_12_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_12_FONT_ID;
|
||||
}
|
||||
if (baseFontId == NOTOSANS_16_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_14_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_12_FONT_ID;
|
||||
}
|
||||
if (baseFontId == NOTOSANS_18_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return NOTOSANS_16_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return NOTOSANS_14_FONT_ID;
|
||||
}
|
||||
|
||||
if (baseFontId == OPENDYSLEXIC_8_FONT_ID) return OPENDYSLEXIC_8_FONT_ID;
|
||||
if (baseFontId == OPENDYSLEXIC_10_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_8_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_8_FONT_ID;
|
||||
}
|
||||
if (baseFontId == OPENDYSLEXIC_12_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_10_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_8_FONT_ID;
|
||||
}
|
||||
if (baseFontId == OPENDYSLEXIC_14_FONT_ID) {
|
||||
if (mode == SmallerFontsMode::SMALLER) return OPENDYSLEXIC_12_FONT_ID;
|
||||
if (mode == SmallerFontsMode::SMALLEST) return OPENDYSLEXIC_10_FONT_ID;
|
||||
}
|
||||
|
||||
return baseFontId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
enum class SmallerFontsMode : uint8_t {
|
||||
MODE_OFF = 0,
|
||||
SMALLER = 1,
|
||||
SMALLEST = 2,
|
||||
};
|
||||
|
||||
namespace SmallerFontsPlugin {
|
||||
const char* modeName(SmallerFontsMode mode);
|
||||
int resolveReaderFontId(int baseFontId, SmallerFontsMode mode);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "SmallerFontsSettingsActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void SmallerFontsSettingsActivity::render(GfxRenderer& renderer, SmallerFontsMode currentMode) {
|
||||
const int LABEL_X = 10;
|
||||
const int VALUE_X = 160;
|
||||
const int ROW_Y = 60;
|
||||
renderer.drawText(UI_12_FONT_ID, LABEL_X, ROW_Y, "Smaller Fonts");
|
||||
renderer.drawText(UI_12_FONT_ID, VALUE_X, ROW_Y, SmallerFontsPlugin::modeName(currentMode));
|
||||
}
|
||||
|
||||
SmallerFontsMode SmallerFontsSettingsActivity::handleNext(SmallerFontsMode currentMode) {
|
||||
uint8_t v = static_cast<uint8_t>(currentMode);
|
||||
if (v < 3) v++;
|
||||
return static_cast<SmallerFontsMode>(v);
|
||||
}
|
||||
|
||||
SmallerFontsMode SmallerFontsSettingsActivity::handlePrev(SmallerFontsMode currentMode) {
|
||||
uint8_t v = static_cast<uint8_t>(currentMode);
|
||||
if (v > 0) v--;
|
||||
return static_cast<SmallerFontsMode>(v);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include "SmallerFontsPlugin.h"
|
||||
|
||||
class SmallerFontsSettingsActivity {
|
||||
public:
|
||||
static void render(GfxRenderer& renderer, SmallerFontsMode currentMode);
|
||||
static SmallerFontsMode handleNext(SmallerFontsMode currentMode);
|
||||
static SmallerFontsMode handlePrev(SmallerFontsMode currentMode);
|
||||
};
|
||||
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import shutil
|
||||
import hashlib
|
||||
import subprocess
|
||||
|
||||
|
||||
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 font_id_from_files(paths):
|
||||
total = 0
|
||||
for p in paths:
|
||||
h = hashlib.sha256(open(p, "rb").read()).hexdigest()
|
||||
total += int(h, 16)
|
||||
return total % (2 ** 32) - (2 ** 31)
|
||||
|
||||
|
||||
def generate_fonts(repo_dir):
|
||||
scripts_dir = os.path.join(repo_dir, "lib", "EpdFont", "scripts")
|
||||
fonts_dir = os.path.join(repo_dir, "lib", "EpdFont", "builtinFonts")
|
||||
source_dir = os.path.join(fonts_dir, "source")
|
||||
fontconvert = os.path.join(scripts_dir, "fontconvert.py")
|
||||
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "freetype-py==2.5.1", "fonttools", "--break-system-packages", "-q"],
|
||||
check=True
|
||||
)
|
||||
|
||||
new_fonts = [
|
||||
("bookerly", 8, "Bookerly", "ttf", ["Regular", "Bold", "Italic", "BoldItalic"]),
|
||||
("bookerly", 10, "Bookerly", "ttf", ["Regular", "Bold", "Italic", "BoldItalic"]),
|
||||
]
|
||||
|
||||
generated = []
|
||||
for family, size, folder, ext, styles in new_fonts:
|
||||
for style in styles:
|
||||
fname = f"{family}_{size}_{style.lower()}"
|
||||
out_path = os.path.join(fonts_dir, f"{fname}.h")
|
||||
if os.path.exists(out_path):
|
||||
print(f" already exists: {fname}.h")
|
||||
generated.append((family, size, style, out_path))
|
||||
continue
|
||||
src_path = os.path.join(source_dir, folder, f"{folder}-{style}.{ext}")
|
||||
if not os.path.exists(src_path):
|
||||
src_path = os.path.join(source_dir, folder, f"{folder.replace('NotoSans','NotoSans')}-{style}.{ext}")
|
||||
extra = ["--2bit", "--compress"] if family != "ubuntu" else []
|
||||
with open(out_path, "w") as f:
|
||||
result = subprocess.run(
|
||||
[sys.executable, fontconvert, fname, str(size), src_path] + extra,
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ERROR generating {fname}: {result.stderr[:200]}")
|
||||
if os.path.exists(out_path):
|
||||
os.remove(out_path)
|
||||
continue
|
||||
if not result.stdout.strip():
|
||||
print(f" ERROR: {fname} generated empty output")
|
||||
if os.path.exists(out_path):
|
||||
os.remove(out_path)
|
||||
continue
|
||||
f.write(result.stdout)
|
||||
print(f" ✓ generated {fname}.h")
|
||||
generated.append((family, size, style, out_path))
|
||||
|
||||
return fonts_dir
|
||||
|
||||
|
||||
def compute_new_font_ids(fonts_dir):
|
||||
ids = {}
|
||||
|
||||
def fid(name, size, suffixes):
|
||||
paths = [os.path.join(fonts_dir, f"{name}_{size}_{s}.h") for s in suffixes]
|
||||
if all(os.path.exists(p) for p in paths):
|
||||
ids[f"{name.upper()}_{size}_FONT_ID"] = font_id_from_files(paths)
|
||||
|
||||
fid("bookerly", 8, ["regular", "bold", "italic", "bolditalic"])
|
||||
fid("bookerly", 10, ["regular", "bold", "italic", "bolditalic"])
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def patch_font_ids_h(repo_dir, new_ids):
|
||||
path = find_first("fontIds.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
lines = []
|
||||
for name, val in new_ids.items():
|
||||
if name not in content:
|
||||
lines.append(f"#define {name} ({val})")
|
||||
|
||||
if not lines:
|
||||
print(" fontIds.h already patched, skipping.")
|
||||
return
|
||||
|
||||
write_file(path, content.rstrip() + "\n" + "\n".join(lines) + "\n")
|
||||
print(f" fontIds.h: added {len(lines)} new font IDs.")
|
||||
|
||||
|
||||
def patch_all_h(repo_dir, new_ids):
|
||||
path = find_first("all.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
includes = {
|
||||
"BOOKERLY_8_FONT_ID": ["bookerly_8_bold", "bookerly_8_bolditalic", "bookerly_8_italic", "bookerly_8_regular"],
|
||||
"BOOKERLY_10_FONT_ID": ["bookerly_10_bold", "bookerly_10_bolditalic", "bookerly_10_italic", "bookerly_10_regular"],
|
||||
}
|
||||
|
||||
added = 0
|
||||
for key, fnames in includes.items():
|
||||
if key not in new_ids:
|
||||
continue
|
||||
for fname in fnames:
|
||||
inc = f'#include <builtinFonts/{fname}.h>'
|
||||
if inc not in content:
|
||||
content += inc + "\n"
|
||||
added += 1
|
||||
|
||||
if added:
|
||||
write_file(path, content)
|
||||
print(f" all.h: added {added} includes.")
|
||||
else:
|
||||
print(" all.h already patched, skipping.")
|
||||
|
||||
|
||||
def patch_main_cpp(repo_dir, new_ids):
|
||||
path = find_first("main.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
font_defs = {
|
||||
"BOOKERLY_8_FONT_ID": ("bookerly", 8, True),
|
||||
"BOOKERLY_10_FONT_ID": ("bookerly", 10, True),
|
||||
}
|
||||
|
||||
decl_lines = []
|
||||
insert_lines = []
|
||||
|
||||
for key, (family, size, has_styles) in font_defs.items():
|
||||
if key not in new_ids:
|
||||
continue
|
||||
varbase = f"{family}{size}"
|
||||
if f"EpdFontFamily {varbase}FontFamily" in content:
|
||||
continue
|
||||
|
||||
decl_lines += [
|
||||
f'EpdFont {varbase}RegularFont(&{family}_{size}_regular);',
|
||||
f'EpdFont {varbase}BoldFont(&{family}_{size}_bold);',
|
||||
f'EpdFont {varbase}ItalicFont(&{family}_{size}_italic);',
|
||||
f'EpdFont {varbase}BoldItalicFont(&{family}_{size}_bolditalic);',
|
||||
f'EpdFontFamily {varbase}FontFamily(&{varbase}RegularFont, &{varbase}BoldFont, &{varbase}ItalicFont, &{varbase}BoldItalicFont);',
|
||||
]
|
||||
insert_lines.append(f' renderer.insertFont({key}, {varbase}FontFamily);')
|
||||
|
||||
if not decl_lines:
|
||||
print(" main.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
anchor_decl = '#ifndef OMIT_FONTS'
|
||||
content = content.replace(anchor_decl, "\n".join(decl_lines) + "\n" + anchor_decl, 1)
|
||||
|
||||
anchor_insert = ' renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);'
|
||||
content = content.replace(anchor_insert, "\n".join(insert_lines) + "\n" + anchor_insert, 1)
|
||||
|
||||
write_file(path, content)
|
||||
print(f" main.cpp: added {len(decl_lines)//5} font families.")
|
||||
|
||||
|
||||
def copy_plugin_sources(plugin_dir, repo_dir):
|
||||
dest = os.path.join(repo_dir, "src", "activities", "settings")
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
|
||||
skip = {"SmallerFontsSettingsPage.h", "SmallerFontsSettingsPage.cpp"}
|
||||
|
||||
for fname in os.listdir(plugin_dir):
|
||||
if fname.endswith((".h", ".cpp", ".hpp")) and fname not in skip:
|
||||
src = os.path.join(plugin_dir, fname)
|
||||
dst = os.path.join(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 "smallerFontsMode" 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/SmallerFontsPlugin.h"'
|
||||
)
|
||||
content = content.replace(
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;',
|
||||
' uint8_t imageRendering = IMAGES_DISPLAY;\n uint8_t smallerFontsMode = 0;'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.h patched.")
|
||||
|
||||
|
||||
def patch_cross_point_settings_cpp(repo_dir):
|
||||
path = find_first("CrossPointSettings.cpp", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "resolveReaderFontId" in content:
|
||||
print(" CrossPointSettings.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'int CrossPointSettings::getReaderFontId() const {',
|
||||
'int CrossPointSettings::getReaderFontId() const {\n auto resolve = [this](int baseId) {\n return SmallerFontsPlugin::resolveReaderFontId(baseId, static_cast<SmallerFontsMode>(smallerFontsMode));\n };\n'
|
||||
)
|
||||
for name in ["BOOKERLY_12", "BOOKERLY_14", "BOOKERLY_16", "BOOKERLY_18",
|
||||
"NOTOSANS_12", "NOTOSANS_14", "NOTOSANS_16", "NOTOSANS_18",
|
||||
"OPENDYSLEXIC_8", "OPENDYSLEXIC_10", "OPENDYSLEXIC_12", "OPENDYSLEXIC_14"]:
|
||||
content = content.replace(f'return {name}_FONT_ID;', f'return resolve({name}_FONT_ID);')
|
||||
|
||||
write_file(path, content)
|
||||
print(" CrossPointSettings.cpp patched.")
|
||||
|
||||
|
||||
def patch_settings_h(repo_dir):
|
||||
path = find_first("SettingsActivity.h", repo_dir)
|
||||
content = read_file(path)
|
||||
|
||||
if "pluginsSettings" in content:
|
||||
print(" SettingsActivity.h already patched, skipping.")
|
||||
return
|
||||
|
||||
content = content.replace(
|
||||
'#include <I18n.h>',
|
||||
'#include <I18n.h>\n#include "SmallerFontsPlugin.h"'
|
||||
)
|
||||
content = content.replace(
|
||||
'static constexpr int categoryCount = 4;',
|
||||
'static constexpr int categoryCount = 5;'
|
||||
)
|
||||
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 "pluginsSettings" in content:
|
||||
print(" SettingsActivity.cpp already patched, skipping.")
|
||||
return
|
||||
|
||||
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};'
|
||||
)
|
||||
content = content.replace(
|
||||
' systemSettings.clear();',
|
||||
' systemSettings.clear();\n pluginsSettings.clear();'
|
||||
)
|
||||
content = content.replace(
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));',
|
||||
' readerSettings.push_back(SettingInfo::Action(StrId::STR_CUSTOMISE_STATUS_BAR, SettingAction::CustomiseStatusBar));\n\n'
|
||||
' pluginsSettings.push_back(SettingInfo::Enum(\n'
|
||||
' StrId::STR_NONE_OPT,\n'
|
||||
' &CrossPointSettings::smallerFontsMode,\n'
|
||||
' {StrId::STR_NONE_OPT, StrId::STR_NONE_OPT, StrId::STR_NONE_OPT},\n'
|
||||
' "smallerFontsMode"\n'
|
||||
' ));'
|
||||
)
|
||||
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;'
|
||||
)
|
||||
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 }'
|
||||
)
|
||||
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);'
|
||||
)
|
||||
content = content.replace(
|
||||
' [&settings](int index) { return std::string(I18N.get(settings[index].nameId)); },',
|
||||
' [&settings, this](int index) -> std::string {\n'
|
||||
' if (selectedCategoryIndex == 4) return "Smaller Fonts";\n'
|
||||
' return std::string(I18N.get(settings[index].nameId));\n'
|
||||
' },'
|
||||
)
|
||||
content = content.replace(
|
||||
' } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {\n const uint8_t value = SETTINGS.*(setting.valuePtr);\n valueText = I18N.get(setting.enumValues[value]);\n }',
|
||||
' } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {\n const uint8_t value = SETTINGS.*(setting.valuePtr);\n if (setting.key && std::string(setting.key) == "smallerFontsMode") {\n valueText = SmallerFontsPlugin::modeName(static_cast<SmallerFontsMode>(value));\n } else {\n valueText = I18N.get(setting.enumValues[value]);\n }\n }'
|
||||
)
|
||||
|
||||
write_file(path, content)
|
||||
print(" SettingsActivity.cpp 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(" Generating smaller font files...")
|
||||
fonts_dir = generate_fonts(repo_dir)
|
||||
|
||||
print(" Computing new font IDs...")
|
||||
new_ids = compute_new_font_ids(fonts_dir)
|
||||
for k, v in new_ids.items():
|
||||
print(f" {k} = {v}")
|
||||
|
||||
print(" Patching fontIds.h...")
|
||||
patch_font_ids_h(repo_dir, new_ids)
|
||||
|
||||
print(" Patching all.h...")
|
||||
patch_all_h(repo_dir, new_ids)
|
||||
|
||||
print(" Patching main.cpp...")
|
||||
patch_main_cpp(repo_dir, new_ids)
|
||||
|
||||
print(" Patching CrossPointSettings.h...")
|
||||
patch_cross_point_settings_h(repo_dir)
|
||||
|
||||
print(" Patching CrossPointSettings.cpp...")
|
||||
patch_cross_point_settings_cpp(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.h...")
|
||||
patch_settings_h(repo_dir)
|
||||
|
||||
print(" Patching SettingsActivity.cpp...")
|
||||
patch_settings_cpp(repo_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit("Usage: python patch.py <path-to-crosspoint-repo>")
|
||||
patch(sys.argv[1])
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Reference in New Issue
Block a user