feat(vegas): one declared participation per plugin (scroll | pause | exclude) (#682)

A plugin takes part in Vegas mode in one declared way: 'scroll', 'pause'
or 'exclude', resolved from the user's vegas_participation setting, the
manifest field, then the legacy hooks, so no plugin changes behaviour.
The stream manager decides inclusion and pauses through it; the installed
plugins API and the Vegas plugin-order list report it. Deprecates
get_supported_vegas_modes, get_vegas_segment_width and vegas_panel_count
for removal in 3.9.0, and regenerates docs/DEPRECATIONS_3.8.md to include
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 09:30:37 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b2df0fda1b
commit 1c928b2033
19 changed files with 1094 additions and 244 deletions
+31 -50
View File
@@ -5,23 +5,25 @@ Manages plugin content streaming with look-ahead buffering. Maintains a queue
of plugin content that's ready to be rendered, prefetching 1-2 plugins ahead
of the current scroll position.
Supports three display modes:
- SCROLL: Continuous scrolling content
- FIXED_SEGMENT: Fixed block that scrolls by
- STATIC: Pause scroll to display (marked for coordinator handling)
Each plugin takes part in one of three ways (its Vegas participation, see
BasePlugin.get_vegas_participation):
- 'scroll': its content joins the strip
- 'pause': the scroll pauses for its turn (a STATIC segment, marked for the
coordinator)
- 'exclude': left out of the rotation
"""
import logging
import threading
import time
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING, cast
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING
from collections import deque
from dataclasses import dataclass, field
from PIL import Image
from src.vegas_mode.config import VegasModeConfig
from src.vegas_mode.plugin_adapter import PluginAdapter
from src.plugin_system.base_plugin import VegasDisplayMode
from src.plugin_system.base_plugin import VegasDisplayMode, resolve_vegas_participation
if TYPE_CHECKING:
from src.plugin_system.plugin_manager import PluginManager
@@ -34,11 +36,12 @@ class ContentSegment:
"""One plugin's content for a cycle.
A STATIC segment carries no images: it marks where the coordinator pauses
the scroll to show the plugin full-screen.
the scroll to show a plugin whose participation is ``'pause'``. Every
other segment is SCROLL.
"""
plugin_id: str
images: List[Image.Image]
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.FIXED_SEGMENT)
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.SCROLL)
class StreamManager:
@@ -335,25 +338,14 @@ class StreamManager:
logger.debug("[%s] Vegas: skipped (not enabled)", plugin_id)
continue
# Content type 'none' is left out, except for STATIC plugins,
# which pause the scroll rather than contributing to it.
content_type = self.plugin_adapter.get_content_type(plugin, plugin_id)
display_mode = VegasDisplayMode.FIXED_SEGMENT
try:
display_mode = plugin.get_vegas_display_mode()
except Exception:
# Plugin error should not abort refresh; use default mode
logger.exception(
"[%s] (%s) get_vegas_display_mode() failed, using default",
plugin_id, plugin.__class__.__name__
)
included = (content_type != 'none'
or display_mode == VegasDisplayMode.STATIC)
# 'pause' plugins stay in the rotation: they pause the scroll
# for their turn rather than contributing to it.
participation = resolve_vegas_participation(plugin, plugin_id)
included = participation != 'exclude'
logger.debug(
"[%s] Vegas: %s (content_type=%s, display_mode=%s)",
"[%s] Vegas: %s (participation=%s)",
plugin_id, "included" if included else "excluded",
content_type, display_mode.value
participation
)
if included:
available_plugins.append(plugin_id)
@@ -580,23 +572,13 @@ class StreamManager:
logger.warning("[%s] Plugin not found in plugin_manager.plugins", plugin_id)
return None
display_mode = VegasDisplayMode.FIXED_SEGMENT
try:
display_mode = plugin.get_vegas_display_mode()
except (AttributeError, TypeError) as e:
logger.debug(
"[%s] get_vegas_display_mode() not available: %s (using FIXED_SEGMENT)",
plugin_id, e
)
# For STATIC mode, we create a placeholder segment
# The actual content will be displayed by coordinator during pause
if display_mode == VegasDisplayMode.STATIC:
# Create minimal placeholder - coordinator handles actual display
# A 'pause' plugin gets a placeholder segment; the coordinator
# draws it with display() when the scroll reaches its turn.
if resolve_vegas_participation(plugin, plugin_id) == 'pause':
segment = ContentSegment(
plugin_id=plugin_id,
images=[], # No images needed for static pause
display_mode=display_mode
display_mode=VegasDisplayMode.STATIC
)
self.stats['segments_fetched'] += 1
logger.debug(
@@ -605,7 +587,6 @@ class StreamManager:
)
return segment
# Get content via adapter for SCROLL/FIXED_SEGMENT modes
images = self.plugin_adapter.get_content(plugin, plugin_id)
if not images:
# The adapter already warns when every content path failed;
@@ -619,13 +600,13 @@ class StreamManager:
segment = ContentSegment(
plugin_id=plugin_id,
images=images,
display_mode=display_mode
display_mode=VegasDisplayMode.SCROLL
)
self.stats['segments_fetched'] += 1
logger.debug(
"[%s] Segment: %d image(s), %dpx, mode=%s",
plugin_id, len(images), total_width, display_mode.value
"[%s] Segment: %d image(s), %dpx",
plugin_id, len(images), total_width
)
return segment
@@ -693,16 +674,16 @@ class StreamManager:
return layout
def is_static_plugin(self, plugin_id: str) -> bool:
"""Whether a loaded plugin asks Vegas to pause for it (STATIC mode)."""
"""Whether a loaded plugin asks Vegas to pause for it (participation 'pause').
Only 'pause' is acted on here. A plugin whose participation has turned
to 'exclude' since the rotation was built is still fetched this cycle,
as it always was; the next refresh drops it.
"""
plugin = getattr(self.plugin_manager, 'plugins', {}).get(plugin_id)
if plugin is None:
return False
try:
return cast(bool, plugin.get_vegas_display_mode() == VegasDisplayMode.STATIC)
except Exception:
logger.debug("[%s] get_vegas_display_mode() failed; treating as not STATIC",
plugin_id, exc_info=True)
return False
return resolve_vegas_participation(plugin, plugin_id) == 'pause'
def take_next_group(
self, count: Optional[int] = None, offscreen_only: bool = False