* refactor: remove the skin system Skins never rendered with the current scoreboard plugins: the only hook was SportsCore._render_game in src/base_classes, which no plugin builds on, so the UI and store already treated them as unsupported. The owner decided on 2026-09-23 to remove them outright. Removed src/skin_system/ (runtime, base class, fixtures), skins/, scripts/validate_skin.py and their tests; the store's "type": "skin" installer, uninstaller and hide/refuse filters (the official registry lists no skins); SchemaManager.inject_skin_selector; and GET /api/v3/skins. Stored skin/skin_options config values are handled in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): drop retired skin/skin_options keys instead of validating them A config.json written while the skin system existed can carry skin and skin_options in any plugin section, and most plugin schemas set additionalProperties: false. They are no longer core plugin properties; RETIRED_PLUGIN_KEYS in schema_manager lists them and drop_retired_plugin_keys removes them (unless the plugin's own schema declares the name) in prepare_plugin_config, which loading, hot reload, GET /plugins/config and both web saves already share, and in validate_config_against_schema for callers that validate a raw section. POST /plugins/config and /config/main also drop them from the stored section they merge into, so they leave config.json on the next save. Tests cover the load path (real PluginManager.load_plugin: no schema warning, not degraded), raw and prepared validation, validate_all_plugin_configs, and the JSON, form and /config/main saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: remove the unused src/base_classes package No scoreboard plugin builds on src.base_classes: the nine monorepo scoreboards ship their own sports.py and share code through src/common (docs/SPORTS_UNIFICATION.md), and none of the third-party registry plugins imports it. The one import anywhere, baseball-scoreboard's rankings_manager.py, is a lazy import of ESPNDataSource in a class nothing instantiates. Removed the package and the eight test files that only tested it (test_api_extractors, test_data_sources, test_sports_base_characterization, test_sports_capabilities, test_sports_core_promotions, test_sports_logo_cache_bounded, test_sports_modes_promotions, test_sports_odds_fanout). test_common_is_hardware_free no longer lists src.base_classes as a forbidden import, and comments in sports_helpers.py and base_odds_manager.py stop pointing at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop the skin system and src/base_classes from the docs Deletes docs/SKIN_SYSTEM.md and docs/CREATING_SKINS.md and every link to them (docs/README.md, README.md, PLUGIN_DEVELOPMENT_GUIDE.md, the /skins section of REST_API_REFERENCE.md), the skin section of CLAUDE.md and the term in PRODUCT.md. SPORTS_UNIFICATION.md now says src/base_classes was removed and shared code lives in src/common, in the Layering section and the view-model-contract rule. Other docs stop pointing at the removed package. CHANGELOG records both removals under Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(store): hide and refuse registry entries that aren't plugins The skin filters went with the skin system, but a custom registry can still list "type": "skin" entries, and installing one as a plugin would unpack it into the plugins directory. PluginStoreManager.is_plugin_entry() (a missing type means plugin) now hides non-plugin entries from the store and custom-registry listings, and install refuses them, in the route with a clear 400 and in _install_plugin_impl for any other caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
12 KiB
FontManager Usage Guide
Picking a size automatically: if you want the largest font that fits a given area rather than a fixed size, use the adaptive layout system's font ladders, which resolve through this FontManager.
BasePluginsubclasses get this asself.layout.fit_text(...); other code can build aLayoutContext(width, height, font_manager)directly — see ADAPTIVE_LAYOUT.md.
Overview
The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:
- Manager font registration and detection
- Plugin font management
- Programmatic per-element font overrides
- Performance monitoring and caching
- Dynamic font discovery
Getting the FontManager
There is one shared FontManager per display process. The display controller
creates it and hands it to the PluginManager, so a plugin reaches it
through its plugin_manager:
class MyPlugin(BasePlugin):
def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
self.font_manager = self._get_font_manager()
BasePlugin._get_font_manager() returns plugin_manager.font_manager, or a
standalone FontManager when none is available (test harnesses, mocks).
DisplayManager has no font_manager attribute —
display_manager.font_manager raises AttributeError.
Architecture
Manager-Centric Design
Managers define their own fonts, but the FontManager:
- Loads and caches fonts for performance
- Detects font usage for visibility
- Allows manual overrides when needed
- Supports plugin fonts with namespacing
Font Resolution Flow
Manager requests font → Check manual overrides → Apply manager choice → Cache & return
For Manager Developers
Basic Font Usage
from src.font_manager import FontManager
class MyManager:
def __init__(self, config, display_manager, cache_manager, plugin_manager):
self.display_manager = display_manager
self.font_manager = plugin_manager.font_manager # Shared FontManager
self.manager_id = "my_manager"
def display(self):
# Define your font choices
element_key = "my_manager.title"
font_family = "press_start"
font_size_px = 10
color = (255, 255, 255) # RGB white
# Register your font choice (for detection and future overrides)
self.font_manager.register_manager_font(
manager_id=self.manager_id,
element_key=element_key,
family=font_family,
size_px=font_size_px,
color=color
)
# Get the font (checks for manual overrides automatically)
font = self.font_manager.resolve_font(
element_key=element_key,
family=font_family,
size_px=font_size_px
)
# Use the font for rendering
self.display_manager.draw_text(
"Hello World",
x=10, y=10,
color=color,
font=font
)
Advanced Font Usage
class AdvancedManager:
def __init__(self, config, display_manager, cache_manager, plugin_manager):
self.display_manager = display_manager
self.font_manager = plugin_manager.font_manager
self.manager_id = "advanced_manager"
# Define your font specifications
self.font_specs = {
"title": {"family": "press_start", "size_px": 12, "color": (255, 255, 0)},
"body": {"family": "four_by_six", "size_px": 8, "color": (255, 255, 255)},
"footer": {"family": "five_by_seven", "size_px": 7, "color": (128, 128, 128)}
}
# Register all font specs
for element_type, spec in self.font_specs.items():
element_key = f"{self.manager_id}.{element_type}"
self.font_manager.register_manager_font(
manager_id=self.manager_id,
element_key=element_key,
family=spec["family"],
size_px=spec["size_px"],
color=spec["color"]
)
def get_font(self, element_type: str):
"""Helper method to get fonts with override support."""
spec = self.font_specs[element_type]
element_key = f"{self.manager_id}.{element_type}"
return self.font_manager.resolve_font(
element_key=element_key,
family=spec["family"],
size_px=spec["size_px"]
)
def display(self):
# Get fonts (automatically checks for overrides)
title_font = self.get_font("title")
body_font = self.get_font("body")
footer_font = self.get_font("footer")
# Render with fonts
self.display_manager.draw_text("Title", font=title_font, color=self.font_specs["title"]["color"])
self.display_manager.draw_text("Body Text", font=body_font, color=self.font_specs["body"]["color"])
self.display_manager.draw_text("Footer", font=footer_font, color=self.font_specs["footer"]["color"])
Using Size Tokens
# Get available size tokens
tokens = self.font_manager.get_size_tokens()
# Returns: {'xs': 6, 'sm': 8, 'md': 10, 'lg': 12, 'xl': 14, 'xxl': 16}
# Use token to get size
size_px = tokens.get('md', 10) # 10px
# Then use in font resolution
font = self.font_manager.resolve_font(
element_key="my_manager.text",
family="press_start",
size_px=size_px
)
For Plugin Developers
Note
: plugins that ship their own fonts via a
"fonts"block inmanifest.jsonare registered automatically during plugin load (src/plugin_system/plugin_manager.pycallsFontManager.register_plugin_fonts()). Theplugin://…source URIs documented below are resolved relative to the plugin's install directory.The web UI's Fonts tab lists, uploads, previews and deletes the font files in
assets/fonts/. It does not show fonts registered throughregister_manager_font()and has no override editor (the override panels and/api/v3/fonts/overridesendpoints were removed). The programmatic override workflow in Manual Font Overrides below still works. Let users pick fonts through your plugin's own config schema.
Plugin Font Registration
In your plugin's manifest.json:
{
"id": "my-plugin",
"name": "My Plugin",
"fonts": {
"fonts": [
{
"family": "custom_font",
"source": "plugin://fonts/custom.ttf",
"metadata": {
"description": "Custom plugin font",
"license": "MIT"
}
},
{
"family": "web_font",
"source": "https://example.com/fonts/font.ttf",
"metadata": {
"description": "Downloaded font",
"checksum": "sha256:abc123..."
}
}
]
}
}
Using Plugin Fonts
class MyPlugin(BasePlugin):
def __init__(self, plugin_id, config, display_manager, cache_manager, plugin_manager):
super().__init__(plugin_id, config, display_manager, cache_manager, plugin_manager)
self.font_manager = self._get_font_manager()
def display(self):
# Use plugin font (automatically namespaced)
font = self.font_manager.resolve_font(
element_key=f"{self.plugin_id}.text",
family="custom_font", # Will be resolved as "my-plugin::custom_font"
size_px=10,
plugin_id=self.plugin_id
)
self.display_manager.draw_text("Plugin Text", font=font)
Manual Font Overrides
Overrides are set in code (there is no web UI or REST endpoint for them).
They are stored in config/font_overrides.json and persist across restarts.
Programmatic Overrides
# Set override
font_manager.set_override(
element_key="nfl.live.score",
family="four_by_six",
size_px=8
)
# Remove override
font_manager.remove_override("nfl.live.score")
# Get all overrides
overrides = font_manager.get_overrides()
Font Discovery
Available Fonts
The FontManager automatically scans assets/fonts/ for TTF and BDF fonts:
# Get all available fonts
fonts = font_manager.get_available_fonts()
# Returns: {'press_start': 'assets/fonts/PressStart2P-Regular.ttf', ...}
# Check if font exists
if "my_font" in fonts:
font = font_manager.get_font("my_font", 10)
Adding Custom Fonts
Place font files in assets/fonts/ directory:
- Supported formats:
.ttf,.bdf - Font family name is derived from filename (without extension)
- Will be automatically discovered on next initialization
Performance Monitoring
# Get performance stats
stats = font_manager.get_performance_stats()
print(f"Cache hit rate: {stats['cache_hit_rate']*100:.1f}%")
print(f"Total fonts cached: {stats['total_fonts_cached']}")
print(f"Failed loads: {stats['failed_loads']}")
print(f"Manager fonts: {stats['manager_fonts']}")
print(f"Plugin fonts: {stats['plugin_fonts']}")
Text Measurement
# Measure text dimensions
width, height, baseline = font_manager.measure_text("Hello", font)
# Get font height
font_height = font_manager.get_font_height(font)
Best Practices
For Managers
- Register all fonts you use for visibility
- Use consistent element keys (e.g.,
{manager_id}.{element_type}) - Cache font references if using same font multiple times
- Use
resolve_font()notget_font()directly to support overrides - Define sensible defaults that work well on LED matrix
For Plugins
- Use plugin-relative paths (
plugin://fonts/...) - Include font metadata (license, description)
- Provide fallback fonts if custom fonts fail to load
- Test with different display sizes
General
- BDF fonts are often better for small sizes on LED matrices
- TTF fonts work well for larger sizes
- Monospace fonts are easier to align
- Test on actual hardware - what looks good on screen may not work on LED matrix
Migration from Old System
Old Way (Direct Font Loading)
self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
New Way (FontManager)
element_key = f"{self.manager_id}.text"
self.font_manager.register_manager_font(
manager_id=self.manager_id,
element_key=element_key,
family="pressstart2p-regular",
size_px=8
)
self.font = self.font_manager.resolve_font(
element_key=element_key,
family="pressstart2p-regular",
size_px=8
)
Troubleshooting
Font Not Found
- Check font file exists in
assets/fonts/ - Verify font family name matches filename (without extension, lowercase)
- Check logs for font discovery errors
Override Not Working
- Verify element key matches exactly what manager registered
- Check
config/font_overrides.jsonfor correct syntax - Restart application to ensure overrides are loaded
Performance Issues
- Check cache hit rate in performance stats
- Reduce number of unique font/size combinations
- Clear cache if it grows too large:
font_manager.clear_cache()
Plugin Fonts Not Loading
- Verify plugin manifest syntax
- Check plugin directory structure
- Review logs for download/registration errors
- Ensure font URLs are accessible
API Reference
FontManager Methods
register_manager_font(manager_id, element_key, family, size_px, color=None)- Register font usageresolve_font(element_key, family, size_px, plugin_id=None)- Get font with override supportget_font(family, size_px)- Get font directly (bypasses overrides)measure_text(text, font)- Measure text dimensionsget_font_height(font)- Get font heightset_override(element_key, family=None, size_px=None)- Set manual overrideremove_override(element_key)- Remove overrideget_overrides()- Get all overridesget_detected_fonts()- Get all detected font usageget_manager_fonts(manager_id=None)- Get fonts by managerget_available_fonts()- Get font catalogget_size_tokens()- Get size token definitionsget_performance_stats()- Get performance metricsclear_cache()- Clear font cacheregister_plugin_fonts(plugin_id, font_manifest)- Register plugin fontsunregister_plugin_fonts(plugin_id)- Unregister plugin fonts
Example: Complete Manager Implementation
For a working example of the font manager API in use, see
src/font_manager.py itself.