"""The contract every quad module implements. Adding a module = drop a file in modules/ that defines a top-level `SPEC` (a ModuleSpec), then append its import to registry.py. Nothing else needs to change: enable/disable, feature toggles, the card chrome, and the expand/collapse plumbing are all provided generically. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Callable, Optional import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 @dataclass class Feature: """A per-quad on/off switch surfaced in the card's settings popover.""" id: str label: str default: bool = True @dataclass class ModuleInstance: """What a module's build() returns.""" compact: Gtk.Widget # shown in the 2x2 cell expanded: Optional[Gtk.Widget] = None # shown when the quad is expanded scroll_expanded: bool = True # wrap the expanded view in a scroller on_show: Optional[Callable[[], None]] = None on_hide: Optional[Callable[[], None]] = None destroy: Optional[Callable[[], None]] = None @dataclass class ModuleSpec: id: str title: str icon: str # nerd-font glyph build: Callable[["ModuleContext"], ModuleInstance] default_enabled: bool = True features: list[Feature] = field(default_factory=list) class ModuleContext: """Handed to a module's build(). Scopes settings to the module and exposes the expand/collapse requests so a module can drive the layout without knowing it.""" def __init__(self, spec: ModuleSpec, settings, services, request_expand: Callable[[str], None], request_collapse: Callable[[], None]): self.spec = spec self.settings = settings self.services = services self._request_expand = request_expand self._request_collapse = request_collapse def expand(self) -> None: self._request_expand(self.spec.id) def collapse(self) -> None: self._request_collapse() # feature toggles, scoped to this module def feature(self, feature_id: str, default: bool = True) -> bool: return self.settings.feature(self.spec.id, feature_id, default) def on_settings_changed(self, cb: Callable[[], None]) -> None: self.settings.subscribe(cb)