aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/app_inventory.py
blob: d493475da3f8bba306342f75ddfc6d3611694433 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""Theme-studio package/app face inventory assembly helpers."""

from __future__ import annotations

import json
import os
from collections.abc import Sequence
from typing import Any

from face_data import BESPOKE_APP_SPECS, PINNED_PACKAGE_FACES


# Keys of the bespoke apps (single-sourced in face_data), excluded from the
# generic-inventory path so they aren't also emitted as plain inventory apps.
# "org" is an explicit alias of the "org-mode" bespoke app, so an inventory
# package literally named "org" never gets a duplicate generic entry.
BESPOKE_APPS = {spec[0] for spec in BESPOKE_APP_SPECS} | {"org"}


# Inventory apps (not in BESPOKE_APPS) default to the generic preview. Apps with
# a dedicated PACKAGE_PREVIEWS renderer in app.js are keyed by name here.
PREVIEW_KEYS = {
    "markdown-mode": "markdown",
    "company": "company",
    "company-box": "companybox",
    "transient": "transient",
    "magit-section": "magitsection",
    "rainbow-delimiters": "rainbowdelims",
    "web-mode": "webmode",
}

# Custom display labels for inventory apps whose package name is an acronym
# worth spelling out (matches the bespoke EAT / LSP / SHR style: full name with
# the acronym in parentheses).
PACKAGE_LABEL_OVERRIDES = {
    "emms": "emacs multimedia system (emms)",
}


def face_label(face: str, prefix: str) -> str:
    label = face[len(prefix) :] if face.startswith(prefix) else face
    return label.replace("-face", "").replace("-", " ")


def face_rows(names: Sequence[str], prefix: str, seed: dict[str, dict[str, Any]]) -> list[list[Any]]:
    return [[face, face_label(face, prefix), seed.get(face, {})] for face in names]


def add_inventory_apps(apps: dict[str, Any], inventory_path: str) -> dict[str, Any]:
    """Add generic editable apps for installed packages not covered by bespoke previews.

    PINNED_PACKAGE_FACES (the ecosystem coverage policy) is the curated record
    of packages retired from this config: a pinned package is always marked
    not loaded, and it survives inventory regeneration -- an uninstall must
    never drop an app from the studio. Its face list stays fresh from the live
    inventory when present (a still-installed dependency can grow faces); the
    pin is the fallback when the inventory no longer carries it.
    """
    inventory: dict[str, Any] = {}
    if os.path.exists(inventory_path):
        with open(inventory_path) as src:
            inventory = json.load(src)
    for pkg in sorted(inventory):
        if pkg in BESPOKE_APPS or pkg in apps:
            continue
        apps[pkg] = {
            "label": PACKAGE_LABEL_OVERRIDES.get(pkg, pkg),
            "preview": PREVIEW_KEYS.get(pkg, "generic"),
            "faces": [[face, face_label(face, pkg + "-"), {}] for face in inventory[pkg]],
        }
    for pkg in sorted(PINNED_PACKAGE_FACES):
        if pkg in BESPOKE_APPS:
            continue
        faces = inventory.get(pkg, PINNED_PACKAGE_FACES[pkg])
        apps[pkg] = {
            "label": PACKAGE_LABEL_OVERRIDES.get(pkg, pkg) + " · not loaded",
            "preview": PREVIEW_KEYS.get(pkg, "generic"),
            "unloaded": True,
            "hover": ("Retired from this config; its faces are pinned so ecosystem "
                      "themes still cover it. The live preview is the only place its "
                      "theming can be seen."),
            "faces": [[face, face_label(face, pkg + "-"), {}] for face in faces],
        }
    return apps


def add_nerd_icons_app(apps: dict[str, Any], inventory_path: str, legend: Any,
                       gallery: Any = None) -> dict[str, Any]:
    """Register nerd-icons as a bespoke legend app from its inventory faces.

    The 34 nerd-icons color faces stay editable rows; LEGEND (the validated rows
    from generate.load_nerd_icons_legend) rides the app so the bespoke previews.js
    renderer can draw each filetype glyph in its mapped face color. GALLERY (the
    full colored catalog grouped by face, from generate.load_nerd_icons_gallery)
    rides alongside when present so the same renderer can draw the gallery section;
    a falsy GALLERY simply omits it (legend-only). A no-op when LEGEND is falsy or
    the inventory lacks nerd-icons -- the caller guards on a valid legend, and
    add_inventory_apps then creates the generic fallback app. Must run before
    add_inventory_apps so the generic path skips nerd-icons.
    """
    if not legend or not os.path.exists(inventory_path):
        return apps
    with open(inventory_path) as src:
        faces = json.load(src).get("nerd-icons")
    if not faces:
        return apps
    app = {
        "label": "nerd-icons",
        "preview": "nerdicons",
        "faces": [[face, face_label(face, "nerd-icons-"), {}] for face in faces],
        "legend": legend,
    }
    if gallery:
        app["gallery"] = gallery
    apps["nerd-icons"] = app
    return apps


def apply_default_face_seeds(apps: dict[str, Any], defaults: Any) -> None:
    if not defaults.available:
        return
    for app in apps.values():
        for row in app["faces"]:
            row[2] = defaults.seed(row[0], False)


def apply_package_overrides(apps: dict[str, Any], packages: dict[str, Any] | None) -> None:
    if not packages:
        return
    for app, package_faces in packages.items():
        if app not in apps:
            continue
        for row in apps[app]["faces"]:
            if row[0] in package_faces:
                row[2] = package_faces[row[0]]