Rewrite plugin installer as a declarative manifest/engine framework for reliable mix-and-match plugin installs, with backup/restore support and assorted plugin fixes (Hardcover file API, Bookerly/Hardcover SettingAction registration, GitHubSync include path, Lockscreen render timing)
This commit is contained in:
1 parent
3ad7a7ab39
commit
fec251f4d1
23 files changed
+2079
-3243
No files matched your search
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Declarative contribution model for xteink plugins.
|
||||
|
||||
The old design had every plugin's patch.py directly rewrite shared CrossPoint
|
||||
source files with hand-written string/regex surgery, and each plugin's patch
|
||||
script *also* hardcoded literal snippets belonging to other plugins (e.g.
|
||||
darkmode's patch.py contained the exact text of the smallerfonts plugin's
|
||||
settings entry, and referenced SmallerFontsPlugin/CrossPointSettings::
|
||||
smallerFontsMode directly) so that things would still line up if both were
|
||||
installed together. That makes every plugin's correctness depend on which
|
||||
*other* plugins happen to be installed and in what order their patches ran -
|
||||
which is exactly why installing a subset (or adding a new plugin) breaks
|
||||
things.
|
||||
|
||||
The fix: plugins no longer touch shared files at all. They describe *what*
|
||||
they need in a shared UI/struct/behaviour via small declarative objects
|
||||
below. A single engine (framework/engine.py) collects the contributions of
|
||||
whichever plugins were actually selected, and applies each shared file
|
||||
exactly once, deriving the whole "Plugins" tab / settings struct / web UI
|
||||
from the current selection instead of guessing at a partially-patched file's
|
||||
history.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceFile:
|
||||
"""A plugin-owned .h/.cpp file to copy into the firmware tree."""
|
||||
src_name: str
|
||||
dest_subdir: str # relative to repo_dir, e.g. "src/activities/settings"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Include:
|
||||
"""An extra #include line needed in one of the shared files."""
|
||||
header: str
|
||||
target: str # "cross_point_settings_h" | "settings_activity_h" | "settings_activity_cpp" | "main_cpp"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsField:
|
||||
"""A member to add to the CrossPointSettings struct (persisted state)."""
|
||||
decl: str # e.g. "uint8_t darkModeState = 0;"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsListEntry:
|
||||
"""A literal SettingInfo::... initializer added to the master settings list
|
||||
(src/SettingsList.h). Used by save/load and by the web UI's full settings
|
||||
dump. Do NOT give these a StrId category other than STR_NONE_OPT - visible
|
||||
Plugins-tab rows are declared via PluginsTabEntry below instead, so they
|
||||
aren't double-counted."""
|
||||
cpp: str # trailing comma, no trailing newline needed
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingActionEnumValue:
|
||||
"""Requests a new enum class SettingAction member."""
|
||||
name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginsTabEntry:
|
||||
"""One row rendered in the on-device 'Plugins' settings tab (and mirrored
|
||||
into the web UI). kind is one of:
|
||||
- "enum": cycles setting.key through option_labels; value_text_expr
|
||||
(a C++ expression using `value`) renders the current value.
|
||||
- "toggle": on/off backed by a uint8_t field.
|
||||
- "string": free text field (e.g. an API token), usually obfuscated.
|
||||
- "action": launches an activity; doesn't read/write a settings field.
|
||||
"""
|
||||
label: str
|
||||
kind: str
|
||||
key: Optional[str] = None
|
||||
option_labels: Optional[List[str]] = None # web UI + on-device generic fallback text
|
||||
value_text_expr: Optional[str] = None # e.g. 'DarkModePlugin::stateName(static_cast<DarkModeState>(value))'
|
||||
obfuscated: bool = False
|
||||
hidden_from_web: bool = False
|
||||
show_on_device: bool = True # False = web/API-only (e.g. an API token field)
|
||||
action_name: Optional[str] = None # must match a SettingActionEnumValue.name
|
||||
action_value_text: str = "Launch"
|
||||
activity_launch_expr: Optional[str] = None # e.g. 'std::make_unique<PongActivity>(renderer, mappedInput)'
|
||||
activity_needs_rebuild: bool = False # result handler should call rebuildSettingsLists()
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnumValueOverride:
|
||||
"""Overrides the on-device valueText for one specific value of an
|
||||
EXISTING built-in enum setting (as opposed to PluginsTabEntry, which
|
||||
describes a brand new plugin-owned setting). Used by e.g. bookerly to
|
||||
make the built-in `fontFamily` setting show "Bookerly" for its new
|
||||
enum value, while every other value keeps rendering normally."""
|
||||
key: str
|
||||
condition_expr: str # e.g. "value == CrossPointSettings::BOOKERLY"
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebOptionAppend:
|
||||
"""Appends one extra dropdown option string to an EXISTING built-in
|
||||
enum setting's web-UI options array (paired with EnumValueOverride
|
||||
above for the on-device side)."""
|
||||
key: str
|
||||
label: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToggleHook:
|
||||
"""Custom logic that runs inside SettingsActivity::toggleCurrentSetting()
|
||||
right after a matching setting's value has been changed, before the
|
||||
generic save+rebuild tail. Used for things like lockscreen's "just
|
||||
enabled, no PIN set yet -> launch PIN-creation activity instead of a
|
||||
plain save" behaviour. `code` may end with `return;` to skip the
|
||||
generic tail entirely for that keypress."""
|
||||
key: str
|
||||
code: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class MainHook:
|
||||
"""A code block inserted into main.cpp's setup(). Multiple plugins'
|
||||
hooks at the same point are concatenated in manifest order - none of
|
||||
them need to know about each other."""
|
||||
point: str # "early_boot" (right after RECENT_BOOKS load, before routing -
|
||||
# display/renderer/fonts are NOT ready yet at this point, so
|
||||
# nothing that renders to screen can run here)
|
||||
# or "post_display_setup" (right after setupDisplayAndFonts(),
|
||||
# before the boot-resume routing/splash paints anything -
|
||||
# display+fonts ARE ready; use this for anything that needs
|
||||
# to draw a screen, e.g. a lock gate, before Home/Reader
|
||||
# becomes visible)
|
||||
# or "post_boot" (once, after the boot-resume routing decision, every boot)
|
||||
code: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformioFlag:
|
||||
line: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationEntry:
|
||||
after_key: str # insert immediately after this existing YAML key, in every translation file
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReaderInvertHook:
|
||||
"""Generalized version of darkmode's screen-invert behaviour: a runtime
|
||||
predicate that, when true, XORs the framebuffer right after each reader
|
||||
page paints (Epub/Txt/Xtc)."""
|
||||
predicate_expr: str
|
||||
include_header: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginManifest:
|
||||
name: str
|
||||
pretty_name: str
|
||||
source_files: List[SourceFile] = field(default_factory=list)
|
||||
includes: List[Include] = field(default_factory=list)
|
||||
settings_fields: List[SettingsField] = field(default_factory=list)
|
||||
settings_list_entries: List[SettingsListEntry] = field(default_factory=list)
|
||||
setting_actions: List[SettingActionEnumValue] = field(default_factory=list)
|
||||
plugins_tab_entries: List[PluginsTabEntry] = field(default_factory=list)
|
||||
enum_value_overrides: List[EnumValueOverride] = field(default_factory=list)
|
||||
web_option_appends: List[WebOptionAppend] = field(default_factory=list)
|
||||
toggle_hooks: List[ToggleHook] = field(default_factory=list)
|
||||
main_hooks: List[MainHook] = field(default_factory=list)
|
||||
platformio_flags: List[PlatformioFlag] = field(default_factory=list)
|
||||
translation_entries: List[TranslationEntry] = field(default_factory=list)
|
||||
reader_invert_hook: Optional[ReaderInvertHook] = None
|
||||
|
||||
# Escape hatches for genuinely plugin-specific work that doesn't touch
|
||||
# shared files (font generation, credential prompts, copying non-source
|
||||
# assets). ctx is a framework.engine.Context.
|
||||
pre_patch: Optional[Callable] = None
|
||||
post_patch: Optional[Callable] = None
|
||||
|
||||
# Ordering hint only (lower runs earlier among pre_patch/post_patch
|
||||
# callbacks - e.g. bookerly's font generation must run before
|
||||
# smallerfonts' generic font-resolution wrapping). Shared-file structural
|
||||
# patches themselves are NOT order-dependent between plugins.
|
||||
phase: int = 0
|
||||
Reference in new issue
Block a user