Files
xteink-plugins/framework/manifest.py
T

188 lines
7.7 KiB
Python
Raw Normal View History

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