diff --git a/.cursorrules b/.cursorrules index b89202dd..9b8ec283 100644 --- a/.cursorrules +++ b/.cursorrules @@ -227,7 +227,7 @@ Common optional fields: ## Plugin Loading Process -1. **Discovery**: PluginManager scans `plugins/` directory for directories containing `manifest.json` +1. **Discovery**: PluginManager scans the configured `plugin_system.plugins_directory` (default `plugin-repos/`) for directories containing `manifest.json` 2. **Validation**: Validates manifest structure and required fields 3. **Loading**: Imports plugin module and instantiates plugin class 4. **Configuration**: Loads plugin config from `config/config.json` @@ -301,7 +301,7 @@ Common optional fields: ## File Organization ``` -plugins/ +plugin-repos/ # default install dir (plugins/ when using dev symlinks) / manifest.json # Plugin metadata manager.py # Main plugin class diff --git a/README.md b/README.md index c0791461..ccc12332 100644 --- a/README.md +++ b/README.md @@ -54,10 +54,11 @@ LEDMatrix is a plugin platform: the displays below are plugins installed from the built-in Plugin Store (web interface → Plugins), where each can be individually enabled, ordered, and configured — display durations, teams, stocks, weather, timezones, and more. The core repo ships with just two -bundled plugins (`starlark-apps` and `web-ui-info`); everything else lives -in the [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) -monorepo and installs with one click. Displays available in the store -include: +bundled plugins (`starlark-apps` and `web-ui-info`); the official plugins +live in the [ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) +monorepo and install with one click, and third-party plugins can be +installed from their own GitHub repositories. Displays available in the +store include: ### Time and Weather - Real-time clock display (2x 64x32 Displays 4mm Pixel Pitch) @@ -428,7 +429,7 @@ I recommend using the web-ui "Quick Actions" to control the Display. ## Plugins
-LEDMatrix uses a plugin-based architecture where all display functionality (except the core calendar) is implemented as plugins. All managers that were previously built into the core system are now available as plugins through the Plugin Store. +LEDMatrix uses a plugin-based architecture where all display functionality is implemented as plugins. All managers that were previously built into the core system are now available as plugins through the Plugin Store. ### Plugin Store See the [Plugin Store documentation](https://github.com/ChuckBuilds/ledmatrix-plugins) for detailed installation instructions. diff --git a/docs/CONFIG_REFERENCE.md b/docs/CONFIG_REFERENCE.md index dbe15681..a7bd81df 100644 --- a/docs/CONFIG_REFERENCE.md +++ b/docs/CONFIG_REFERENCE.md @@ -55,7 +55,7 @@ in `DisplayManager` (`src/display_manager.py`, ~lines 270–295). | `chain_length` | int, `2` | | `parallel` | int, `1` | | `brightness` | int, `90` | -| `hardware_mapping` | string, `"adafruit-hat"` | +| `hardware_mapping` | string, `"adafruit-hat"` (code default `"adafruit-hat-pwm"`) | | `scan_mode` | int, `0` | | `pwm_bits` | int, `9` (code default 10) | | `pwm_dither_bits` | int, `1` | diff --git a/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md b/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md index ffaeda18..d1426979 100644 --- a/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md +++ b/docs/PLUGIN_REGISTRY_SETUP_GUIDE.md @@ -333,7 +333,7 @@ python3 scripts/setup_plugin_repos.py python3 scripts/audit_plugins.py # Validate a single plugin -python3 scripts/check_plugin.py +python3 scripts/check_plugin.py --plugin ``` Registry regeneration (`update_registry.py`) lives in the diff --git a/scripts/fix_perms/fix_assets_permissions.sh b/scripts/fix_perms/fix_assets_permissions.sh old mode 100644 new mode 100755 diff --git a/scripts/fix_perms/fix_cache_permissions.sh b/scripts/fix_perms/fix_cache_permissions.sh old mode 100644 new mode 100755 diff --git a/scripts/fix_perms/fix_plugin_permissions.sh b/scripts/fix_perms/fix_plugin_permissions.sh old mode 100644 new mode 100755 diff --git a/scripts/fix_perms/fix_web_permissions.sh b/scripts/fix_perms/fix_web_permissions.sh old mode 100644 new mode 100755 diff --git a/scripts/install_dependencies_apt.py b/scripts/install_dependencies_apt.py index a913b6ee..52631e8d 100644 --- a/scripts/install_dependencies_apt.py +++ b/scripts/install_dependencies_apt.py @@ -44,7 +44,7 @@ def install_via_apt(package_name: str) -> Tuple[bool, str]: apt_package_map = { 'flask': 'python3-flask', 'PIL': 'python3-pil', - 'freetype': 'python3-freetype', + 'freetype-py': 'python3-freetype', 'psutil': 'python3-psutil', 'werkzeug': 'python3-werkzeug', 'numpy': 'python3-numpy', @@ -95,7 +95,9 @@ def install_via_pip(package_name: str) -> Tuple[bool, str]: # Distribution (pip/apt) names whose importable module name differs. -IMPORT_NAME_MAP = {} +IMPORT_NAME_MAP = { + 'freetype-py': 'freetype', +} def check_package_installed(package_name: str) -> bool: @@ -139,7 +141,7 @@ def main(): required_packages = [ 'flask', 'PIL', - 'freetype', + 'freetype-py', 'psutil', 'werkzeug', 'numpy', diff --git a/test/test_doc_links.py b/test/test_doc_links.py index ac95ea37..08f904af 100644 --- a/test/test_doc_links.py +++ b/test/test_doc_links.py @@ -6,20 +6,23 @@ links inside fenced code blocks. """ import re from pathlib import Path +from typing import Iterator PROJECT_ROOT = Path(__file__).resolve().parent.parent LINK_RE = re.compile(r'\[[^\]]*\]\(([^)\s]+)\)') FENCE_RE = re.compile(r'^(```|~~~)') -def _md_files(): +def _md_files() -> Iterator[Path]: + """Yield active markdown files (repo root + docs/, excluding docs/archive/).""" yield from PROJECT_ROOT.glob('*.md') for path in PROJECT_ROOT.glob('docs/**/*.md'): if 'archive' not in path.parts: yield path -def test_relative_markdown_links_resolve(): +def test_relative_markdown_links_resolve() -> None: + """Every relative markdown link outside code fences must resolve on disk.""" broken = [] for md in _md_files(): in_fence = False diff --git a/test/test_template_targets.py b/test/test_template_targets.py index 4a26d9ff..a720e92e 100644 --- a/test/test_template_targets.py +++ b/test/test_template_targets.py @@ -6,28 +6,32 @@ templates were removed when those displays became plugins). """ import re from pathlib import Path +from typing import Iterator PROJECT_ROOT = Path(__file__).resolve().parent.parent TEMPLATE_ROOT = PROJECT_ROOT / 'web_interface' / 'templates' RENDER_RE = re.compile(r"""render_template\(\s*['"]([^'"]+)['"]""") -def _python_sources(): +def _python_sources() -> Iterator[Path]: + """Yield every Python source that can call render_template().""" yield PROJECT_ROOT / 'web_interface' / 'app.py' yield from (PROJECT_ROOT / 'web_interface' / 'blueprints').glob('*.py') -def test_all_literal_render_template_targets_exist(): +def test_all_literal_render_template_targets_exist() -> None: + """Every string-literal render_template() target must exist on disk.""" missing = [] for source in _python_sources(): text = source.read_text(encoding='utf-8') - for lineno, line in enumerate(text.splitlines(), 1): - for target in RENDER_RE.findall(line): - if not (TEMPLATE_ROOT / target).is_file(): - missing.append( - f'{source.relative_to(PROJECT_ROOT)}:{lineno} -> {target}' - ) + for match in RENDER_RE.finditer(text): + target = match.group(1) + if not (TEMPLATE_ROOT / target).is_file(): + lineno = text.count('\n', 0, match.start()) + 1 + missing.append( + f'{source.relative_to(PROJECT_ROOT)}:{lineno} -> {target}' + ) assert not missing, ( 'render_template() references templates that do not exist under ' - f'web_interface/templates/:\n' + '\n'.join(missing) + 'web_interface/templates/:\n' + '\n'.join(missing) ) diff --git a/test/test_widget_scripts.py b/test/test_widget_scripts.py index 8c07a8b0..58e949b3 100644 --- a/test/test_widget_scripts.py +++ b/test/test_widget_scripts.py @@ -6,12 +6,19 @@ config schema declares that widget (the field renders as an empty container that polls the registry forever). base.html's widget list is maintained by hand, so this test keeps it honest. """ +import re from pathlib import Path +from typing import Set PROJECT_ROOT = Path(__file__).resolve().parent.parent WIDGETS_DIR = PROJECT_ROOT / 'web_interface' / 'static' / 'v3' / 'js' / 'widgets' BASE_HTML = PROJECT_ROOT / 'web_interface' / 'templates' / 'v3' / 'base.html' +# Matches url_for('static', filename='...') inside actual