from __future__ import annotations import re from collections.abc import Callable from dataclasses import dataclass from typing import Any MODULE_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$") def validate_module_id(module_id: str) -> None: if not MODULE_ID_RE.fullmatch(module_id): raise ValueError( f"invalid module id {module_id!r}: must match [a-z][a-z0-9_]*" ) @dataclass(frozen=True) class BarWidget: id: str factory: Callable[..., object] default_location: int = 0 default_bar_id: int = 0 default_visible: bool = True name: str = "" description: str = "" @dataclass(frozen=True) class SettingsTab: id: str title: str icon: str order: int group: str factory: Callable[[], object] @dataclass(frozen=True) class Module: id: str requires: tuple[str, ...] = () provides: tuple[str, ...] = () needs: tuple[str, ...] = () settings: str | None = None setup: Callable[[Any], None] | None = None settings_tabs: tuple[SettingsTab, ...] = () bar_widgets: tuple[BarWidget, ...] = () styles: str | None = None nix_options: str | None = None enabled_by_default: bool = True def __post_init__(self) -> None: validate_module_id(self.id) if self.settings is not None and not self.settings: raise ValueError("settings file path must not be empty")