Files

188 lines
7.7 KiB
Python

"""
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