#!/usr/bin/env python3 from __future__ import annotations import argparse import json import re import sys import tomllib from pathlib import Path from typing import Any DEFAULT_ROOT = Path(__file__).resolve().parents[2] SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$") LAUNCHER_PREFIX_RE = re.compile(r"^[a-z]+$") DESCRIPTION_MAX_CHARS = 120 ALLOWED_TAGS = { "ai", "animation", "arch", "audio", "bar", "clock", "countdown", "demo", "debian", "desktop", "development", "emoticon", "fedora", "fun", "gaming", "gentoo", "hardware", "hyprland", "indicator", "labwc", "language", "launcher", "mangowc", "media", "music", "network", "niri", "nixos", "opensuse", "panel", "privacy", "productivity", "recording", "service", "shortcut", "sway", "system", "theming", "time", "utility", "video", "void", "wallpaper", } # An id segment must be a lowercase flat identifier: the part after the "/" is also the # plugin's directory here, its export directory on disk, and its slug on the website. ID_SEGMENT_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$") # Names the website reserves for its own routes; a plugin folder cannot take one. RESERVED_NAMES = {"license", "readme", "index", "api", "admin", "static", "assets"} # The store flattens every plugin's translations under its id and rejects keys that are not # lowercase. A dotted label_key is a path of segments; each segment allows a-z, 0-9, dashes, # and underscores, but an underscore may not lead a segment. Uppercase (e.g. "zh-Hans") is out. TRANSLATION_KEY_SEGMENT_RE = re.compile(r"[a-z0-9-][a-z0-9_-]*") # Rule for a dotted path, as written in a manifest label_key/description_key. TRANSLATION_KEY_RULE = ( "keys must be lowercase and contain only a-z, 0-9, dots, dashes, and non-leading underscores" ) # Rule for a single object key inside translations/en.json. The dot is a path separator, so it # cannot appear inside a key: the i18n platform expands "a.b" into nested objects on the next # sync, and a flat dotted key would silently churn. Nest with objects instead. TRANSLATION_SEGMENT_RULE = ( "each translations/en.json key must be a single lowercase segment (a-z, 0-9, dashes, " "non-leading underscores) with no dots; express nesting with objects, not dotted keys" ) # Files every published plugin ships: the site renders the README as the plugin page and # the thumbnail as its card, and the English catalog backs every label_key. REQUIRED_PLUGIN_FILES = ("README.md", "thumbnail.webp", "translations/en.json") THUMBNAIL_MAX_BYTES = 512 * 1024 # The store and the website lay out plugin cards on a fixed 16:9 grid, so the thumbnail # generator exports exactly this. THUMBNAIL_SIZE = (960, 540) THUMBNAIL_GENERATOR_URL = "https://assets.noctalia.dev/plugins/thumbnail-generator.html" WEBP_MAGIC_PREFIX = b"RIFF" WEBP_MAGIC_FORMAT = b"WEBP" # Enough of the file to cover the RIFF header plus the first chunk header and the widest # dimension field of any WebP variant. WEBP_HEADER_BYTES = 32 ROOT_STRING_FIELDS = ( "id", "name", "version", "author", "license", "icon", "description", ) ROOT_ARRAY_FIELDS = ("dependencies", "tags") ENTRY_TYPES = ("widget", "panel", "shortcut", "desktop_widget", "launcher_provider", "service") SETTING_OWNER_TYPES = ("widget", "panel", "desktop_widget", "launcher_provider") SETTING_TYPES = {"string", "string_list", "bool", "glyph", "select", "folder", "file", "int", "color"} PANEL_PLACEMENTS = {"attached", "floating"} PANEL_KEYBOARD_FOCUS = {"on_demand", "exclusive", "none"} PANEL_POSITIONS = { "auto", "center", "top_left", "top_center", "top_right", "center_left", "center_right", "bottom_left", "bottom_center", "bottom_right", } ROOT_FIELDS = set(ROOT_STRING_FIELDS) | set(ROOT_ARRAY_FIELDS) | set(ENTRY_TYPES) | { "setting", "deprecated", "plugin_api", } BASE_ENTRY_FIELDS = {"id", "entry"} ENTRY_FIELDS = { "widget": BASE_ENTRY_FIELDS | {"setting"}, "panel": BASE_ENTRY_FIELDS | { "setting", "width", "height", "placement", "position", "open_near_click", "dismiss_on_outside_click", "keyboard_focus", "persistent", "capture_keys", }, "desktop_widget": BASE_ENTRY_FIELDS | {"setting"}, "service": BASE_ENTRY_FIELDS, "shortcut": BASE_ENTRY_FIELDS, "launcher_provider": BASE_ENTRY_FIELDS | {"prefix", "glyph", "include_in_global_search", "debounce_ms", "setting", "category"}, } CATEGORY_FIELDS = {"label", "glyph"} SETTING_FIELDS = { "key", "type", "label_key", "description_key", "default", "options", "min", "max", "step", "visible_when", "advanced", } OPTION_FIELDS = {"value", "label_key"} VISIBLE_WHEN_FIELDS = {"key", "values"} # Raw HTML is not supported on plugin pages. Markdown autolinks such as # do not match this expression. HTML_RE = re.compile( r"