From 63ba6d44f794a02b64f0929a378250ad8a5523b0 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 2 Sep 2026 13:36:41 +0800 Subject: [PATCH] refactor: basically finish core refactor --- tool/src/aurora_iconset_builder/__init__.py | 51 ++++-- .../aurora_iconset_builder/assemblicon/cli.py | 10 +- .../assemblicon/geometry.py | 153 ++++++++++++------ .../assemblicon/highlevel/context.py | 21 +-- .../assemblicon/highlevel/environment.py | 15 +- .../assemblicon/highlevel/utils.py | 2 +- .../assemblicon/logger.py | 4 + .../assemblicon/lowlevel/artproc.py | 69 +++++--- .../assemblicon/lowlevel/artrdr.py | 28 ++-- .../nostd_icons/__init__.py | 4 +- .../std_icons/__init__.py | 4 +- 11 files changed, 244 insertions(+), 117 deletions(-) diff --git a/tool/src/aurora_iconset_builder/__init__.py b/tool/src/aurora_iconset_builder/__init__.py index a9965d4..dff8fbb 100644 --- a/tool/src/aurora_iconset_builder/__init__.py +++ b/tool/src/aurora_iconset_builder/__init__.py @@ -1,11 +1,39 @@ -import logging from pathlib import Path from dataclasses import dataclass -from .assemblicon.cli import ArgParserSetup, create_argparser, parse_standard_opts +from typing import Optional +from .assemblicon.cli import ( + SvgRenderKind, + StdOptsSetup, + create_argparser, + parse_standard_opts, +) from .assemblicon.highlevel.environment import Prologue, Environment from .assemblicon.highlevel.context import Context -from .std_icons import build_standard_icons -from .nostd_icons import build_non_standard_icons +from .assemblicon.logger import APP_LOGGER as LOGGER +from . import std_icons, nostd_icons + + +@dataclass(frozen=True) +class AuroraOpts: + selection: Optional[list[str]] + """The list holding all name of jobs for building.""" + svg_render: SvgRenderKind + """The render kind for SVG file.""" + + +def parse_opts() -> AuroraOpts: + setup = StdOptsSetup(require_input=False, require_output=False) + + parser = create_argparser(setup) + parser.prog = "Aurora Icon Set Builder" + parser.description = "The Programmatic Icon Assembly Workflow for Aurora Icon Set." + + ns = parser.parse_args() + opts = parse_standard_opts(setup, ns) + + return AuroraOpts( + selection=opts.selection, svg_render=opts.svg_render.to_assemblicon_type() + ) @dataclass(frozen=True) @@ -16,17 +44,12 @@ class AuroraPrologue(Prologue): def main() -> None: - parser_cfg = ArgParserSetup(require_input=False, require_output=False) - parser = create_argparser(parser_cfg) - ns = parser.parse_args() - opts = parse_standard_opts(parser_cfg, ns) + opts = parse_opts() repository_root = Path(__file__).resolve().parent.parent.parent.parent iconset_input = repository_root / "src" iconset_output = repository_root / "artifact" - prologue = AuroraPrologue( - iconset_input, iconset_output, opts.svg_render.to_assemblicon_type() - ) + prologue = AuroraPrologue(iconset_input, iconset_output, opts.svg_render) env = Environment(prologue) ctx = Context(env) @@ -34,9 +57,9 @@ def main() -> None: register(ctx) ctx.run(opts.selection) except BaseException as e: - logging.fatal("Runtime error: %s", e) + LOGGER.fatal("Runtime error: %s", e) def register(ctx: Context) -> None: - build_standard_icons(ctx) - build_non_standard_icons(ctx) + std_icons.register(ctx) + nostd_icons.register(ctx) diff --git a/tool/src/aurora_iconset_builder/assemblicon/cli.py b/tool/src/aurora_iconset_builder/assemblicon/cli.py index 2e6e58a..f969711 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/cli.py +++ b/tool/src/aurora_iconset_builder/assemblicon/cli.py @@ -33,14 +33,14 @@ class StdOpts: @dataclass(frozen=True) -class ArgParserSetup: +class StdOptsSetup: require_input: bool """Whether add input path option in argument parser.""" require_output: bool """Whether add outputput path option in argument parser.""" -def create_argparser(cfg: ArgParserSetup) -> ArgumentParser: +def create_argparser(setup: StdOptsSetup) -> ArgumentParser: """ Create Assemblicon standard argument parser. @@ -54,7 +54,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser: prog="Assemblicon", description="The Programmatic Icon Assembly Workflow.", ) - if cfg.require_input: + if setup.require_input: parser.add_argument( "-i", "--input", @@ -65,7 +65,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser: help="The path to input directory.", metavar="DIR", ) - if cfg.require_output: + if setup.require_output: parser.add_argument( "-o", "--output", @@ -99,7 +99,7 @@ def create_argparser(cfg: ArgParserSetup) -> ArgumentParser: return parser -def parse_standard_opts(cfg: ArgParserSetup, ns: Namespace) -> StdOpts: +def parse_standard_opts(cfg: StdOptsSetup, ns: Namespace) -> StdOpts: return StdOpts( input_directory=ns.input if cfg.require_input else None, output_directory=ns.output if cfg.require_output else None, diff --git a/tool/src/aurora_iconset_builder/assemblicon/geometry.py b/tool/src/aurora_iconset_builder/assemblicon/geometry.py index 95f4519..1c0ab43 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/geometry.py +++ b/tool/src/aurora_iconset_builder/assemblicon/geometry.py @@ -1,60 +1,121 @@ +import math from dataclasses import dataclass @dataclass -class MinMaxRect: - x1: int - y1: int - x2: int - y2: int +class Point: + """A point (or vector) on 2D plane with integer coordinates.""" - def to_pos_size_rect(self) -> "PosSizeRect": - return PosSizeRect( - x=self.x1, y=self.y1, width=self.x2 - self.x1, height=self.y2 - self.y1 - ) - - def to_pillow_tuple(self) -> tuple[int, int, int, int]: - return (self.x1, self.y1, self.x2, self.y2) - -@dataclass -class PosSizeRect: x: int y: int - width: int - height: int - def to_min_max_rect(self) -> MinMaxRect: - return MinMaxRect( - x1=self.x, y1=self.y, x2=self.x + self.width, y2=self.y + self.height - ) + @staticmethod + def from_tuple(t: tuple[int, int]) -> "Point": + return Point(t[0], t[1]) - def to_pillow_tuple(self) -> tuple[int, int]: + def __add__(self, other: "Point") -> "Point": + if not isinstance(other, Point): + return NotImplemented + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other: "Point") -> "Point": + if not isinstance(other, Point): + return NotImplemented + return Point(self.x - other.x, self.y - other.y) + + def __neg__(self) -> "Point": + return Point(-self.x, -self.y) + + def __mul__(self, other: int | float) -> "Point": + if not isinstance(other, (int, float)): + return NotImplemented + return Point(math.floor(self.x * other), math.floor(self.y * other)) + + def __rmul__(self, other: int | float) -> "Point": + return self.__mul__(other) + + def __floordiv__(self, other: int | float) -> "Point": + if not isinstance(other, (int, float)): + return NotImplemented + return Point(math.floor(self.x / other), math.floor(self.y / other)) + + def to_tuple(self) -> tuple[int, int]: + """ + Create the tuple representing this point by ``(x, y)`` + which can be used by Pillow. + """ return (self.x, self.y) -@dataclass -class MarginRect: - top: int - right: int - bottom: int - left: int - - def to_min_max_rect(self, width: int, height: int) -> MinMaxRect: - return MinMaxRect( - x1=self.left, - y1=self.top, - x2=width - self.right, - y2=height - self.bottom, - ) - @dataclass -class PaddingRect: - top: int - right: int - bottom: int - left: int +class Rectangle: + """ + A rectangle framed by two corner points. - def to_min_max_rect(self, x: int, y: int) -> MinMaxRect: - return MinMaxRect( - x1=x - self.left, y1=y - self.top, x2=x + self.right, y2=y + self.bottom - ) + pos1 is always assumed to be the top-left corner and pos2 the + bottom-right one. This relation is a pure convention and is never + validated: constructing a Rectangle with swapped or inverted corners + produces a meaningless-but-silent result. + """ + + pos1: Point + pos2: Point + + @staticmethod + def from_tuple(t: tuple[int, int, int, int]) -> "Rectangle": + return Rectangle(Point(t[0], t[1]), Point(t[2], t[3])) + + @staticmethod + def from_min_max_rect(pos1: Point, pos2: Point) -> "Rectangle": + return Rectangle(pos1, pos2) + + @staticmethod + def from_pos_size_rect(pos: Point, width: int, height: int) -> "Rectangle": + return Rectangle(pos, pos + Point(width, height)) + + @staticmethod + def from_margin_rect( + width: int, height: int, top: int, right: int, bottom: int, left: int + ) -> "Rectangle": + """ + TODO + + :param width: The width of parent. + :param height: The height of parent. + :param top: The distance from this rectangle to the top border of parent. + :param right: The distance from this rectangle to the right border of parent. + :param bottom: The distance from this rectangle to the bottom border of parent. + :param left: The distance from this rectangle to the top border of parent. + """ + return Rectangle(Point(left, top), Point(width - right, height - bottom)) + + @staticmethod + def from_padding_rect( + x: int, y: int, top: int, right: int, bottom: int, left: int + ) -> "Rectangle": + """ + TODO + + :param x: The X factor of reference point. + :param y: The Y factor of reference point. + :param top: The distance from reference point to this rectangle's top border. + :param right: The distance from reference point to this rectangle's right border. + :param bottom: The distance from reference point to this rectangle's bottom border. + :param left: The distance from reference point to this rectangle's left border. + """ + return Rectangle(Point(x - left, y - top), Point(x + right, y + bottom)) + + @property + def width(self) -> int: + return self.pos2.x - self.pos1.x + + @property + def height(self) -> int: + return self.pos2.y - self.pos1.y + + def to_tuple(self) -> tuple[int, int, int, int]: + """ + Create the tuple representing this rectangle by ``(x1, y2 x2, y2)`` + which can be used by Pillow. + """ + return (self.pos1.x, self.pos1.y, self.pos2.x, self.pos2.y) diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py index a36cc8d..b988421 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/context.py @@ -1,29 +1,32 @@ from collections import deque from dataclasses import dataclass -from typing import Callable, Iterable -from .environment import Environment +from typing import Callable, Iterable, TypeVar, Generic +from .environment import Environment, Prologue from ..logger import HIGHLEVEL_LOGGER as LOGGER +P = TypeVar("P", bound=Prologue) + + @dataclass(frozen=True) -class Job: +class Job(Generic[P]): """The class represents a job.""" name: str """The name of this job.""" dependencies: set[str] """The dependencies of this job. Each item is a job name. Empty for no dependencies.""" - executor: Callable[[Environment], None] + executor: Callable[[Environment[P]], None] """The executor of this job.""" -class Context: - __jobs: dict[str, Job] +class Context(Generic[P]): + __jobs: dict[str, Job[P]] """The dict storing all jobs. Key is job name and value is job payload.""" - __env: Environment + __env: Environment[P] """The shared environment passed to every job executor.""" - def __init__(self, env: Environment) -> None: + def __init__(self, env: Environment[P]) -> None: self.__jobs = {} self.__env = env @@ -92,7 +95,7 @@ class Context: self, name: str, deps: Iterable[str], - exec: Callable[[Environment], None], + exec: Callable[[Environment[P]], None], ) -> None: """ Register a new job with given name, dependencies and executor. diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py index 4d3eb81..14ac43d 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/environment.py @@ -6,9 +6,8 @@ from types import TracebackType from dataclasses import dataclass from pathlib import Path from typing import TypeVar, Generic -from .utils import Artifact, ArtifactFile, VART_HW +from .utils import Artifact, ArtifactFile, VART_HW, check_vart from ..logger import HIGHLEVEL_LOGGER as LOGGER -from ..lowlevel.artrdr import SvgRenderKind from ..lowlevel.artrdr import SvgRenderKind, SvgConfig, BlenderConfig from ..lowlevel import artio, artrdr @@ -120,6 +119,14 @@ class Environment(Generic[P]): # endregion + # region: Prologue Visitor + + @property + def prologue(self) -> P: + return self.__prologue + + # endregion + # region: New Artifact def new_artifact(self) -> Artifact: @@ -309,6 +316,8 @@ class Environment(Generic[P]): 'Saving FreeDesktop artifact with kind "%s" and name "%s".', kind, name ) + check_vart(art) + for hw, thumbnail in zip( _FD_THUMBNAIL_HWS, artrdr.render_thumbnail(art, _FD_THUMBNAIL_HWS) ): @@ -343,6 +352,7 @@ class Environment(Generic[P]): LOGGER.info( 'Saving Windows-only artifact with kind "%s" and name "%s".', kind, name ) + check_vart(art) artrdr.render_ico(art, self.__win_artifact(kind, name)) def dup_win_artifact( @@ -372,6 +382,7 @@ class Environment(Generic[P]): LOGGER.info( 'Saving macOS-only artifact with kind "%s" and name "%s".', kind, name ) + check_vart(art) artrdr.render_icns(art, self.__mac_artifact(kind, name)) def dup_mac_artifact( diff --git a/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py index 57abbf3..cd207fa 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py +++ b/tool/src/aurora_iconset_builder/assemblicon/highlevel/utils.py @@ -4,7 +4,7 @@ VART_HW: int = 1024 """The height or width of Vanilla Artifact.""" -def check_vanilla_artifact(art: Artifact) -> None: +def check_vart(art: Artifact) -> None: if art.size != (VART_HW, VART_HW): raise ValueError( f"The size of general highlevel used artifact must be {VART_HW}x{VART_HW}, got {art.width}x{art.height} instead" diff --git a/tool/src/aurora_iconset_builder/assemblicon/logger.py b/tool/src/aurora_iconset_builder/assemblicon/logger.py index bead702..f5ca31c 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/logger.py +++ b/tool/src/aurora_iconset_builder/assemblicon/logger.py @@ -8,6 +8,9 @@ class LoggerFactory: format="[%(levelname)s][%(name)s] %(message)s", level=logging.INFO ) + def get_app_logger(self) -> logging.Logger: + return logging.getLogger("App") + def get_lowlevel_logger(self) -> logging.Logger: return logging.getLogger("Lowlevel") @@ -18,3 +21,4 @@ class LoggerFactory: LOGGER_FACTORY = LoggerFactory() LOWLEVEL_LOGGER = LOGGER_FACTORY.get_lowlevel_logger() HIGHLEVEL_LOGGER = LOGGER_FACTORY.get_highlevel_logger() +APP_LOGGER = LOGGER_FACTORY.get_app_logger() diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py index 286877c..c415643 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artproc.py @@ -1,6 +1,7 @@ import enum import math from .utils import Artifact +from ..geometry import Point class HorizontalAnchor(enum.IntEnum): @@ -15,14 +16,32 @@ class VerticalAnchor(enum.IntEnum): Bottom = enum.auto() -def _anchor_offset(oversize: int, anchor: HorizontalAnchor | VerticalAnchor) -> int: - match anchor: - case HorizontalAnchor.Left | VerticalAnchor.Top: - return 0 - case HorizontalAnchor.Center | VerticalAnchor.Center: - return oversize // 2 - case HorizontalAnchor.Right | VerticalAnchor.Bottom: - return oversize +def anchor_to_offset( + oversize: Point, + horizontal_anchor: HorizontalAnchor, + vertical_anchor: VerticalAnchor, +) -> Point: + half_oversize = oversize // 2 + + x: int + match horizontal_anchor: + case HorizontalAnchor.Left: + x = 0 + case HorizontalAnchor.Center: + x = half_oversize.x + case HorizontalAnchor.Right: + x = oversize.x + + y: int + match vertical_anchor: + case VerticalAnchor.Top: + y = 0 + case VerticalAnchor.Center: + y = half_oversize.y + case VerticalAnchor.Bottom: + y = oversize.y + + return Point(x, y) def aspect_ratio_scale_and_crop( @@ -45,16 +64,20 @@ def aspect_ratio_scale_and_crop( scaled_height = math.ceil(art.height * scale) scaled = art.resize((scaled_width, scaled_height), scale_resample) - crop_x = _anchor_offset(scaled_width - target_width, crop_horizontal_anchor) - crop_y = _anchor_offset(scaled_height - target_height, crop_vertical_anchor) + crop = anchor_to_offset( + Point(scaled_width - target_width, scaled_height - target_height), + crop_horizontal_anchor, + crop_vertical_anchor, + ) + (crop_x, crop_y) = crop.to_tuple() return scaled.crop((crop_x, crop_y, crop_x + target_width, crop_y + target_height)) -def anchor_to_pos( +def art_anchor_to_offset( art: Artifact, horizontal_anchor: HorizontalAnchor, vertical_anchor: VerticalAnchor, -) -> tuple[int, int]: +) -> Point: """ Resolve the anchor point position inside given artifact. @@ -65,17 +88,16 @@ def anchor_to_pos( anchors this is the corresponding edge; for Right/Bottom anchors this is just outside the artifact (the exact corner coordinate). """ - return ( - _anchor_offset(art.width, horizontal_anchor), - _anchor_offset(art.height, vertical_anchor), + return anchor_to_offset( + Point.from_tuple(art.size), horizontal_anchor, vertical_anchor ) def align_paste( art: Artifact, clipboard: Artifact, - art_pos: tuple[int, int], - clipboard_pos: tuple[int, int], + art_pos: Point, + clipboard_pos: Point, ) -> None: """ Paste clipboard onto given artifact with two anchor points aligned. @@ -93,21 +115,16 @@ def align_paste( :param clipboard_pos: The (x, y) point on the clipboard to be aligned, usually resolved by anchor_to_pos. """ - art.paste( - clipboard, - (art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1]), - ) + art.paste(clipboard, (art_pos - clipboard_pos).to_tuple()) def align_alpha_composite( art: Artifact, clipboard: Artifact, - art_pos: tuple[int, int], - clipboard_pos: tuple[int, int], + art_pos: Point, + clipboard_pos: Point, ) -> None: """ Alpha-supported ``align_paste``. """ - art.alpha_composite( - clipboard, (art_pos[0] - clipboard_pos[0], art_pos[1] - clipboard_pos[1]) - ) + art.alpha_composite(clipboard, (art_pos - clipboard_pos).to_tuple()) diff --git a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py index d102a9f..0dfe6cb 100644 --- a/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py +++ b/tool/src/aurora_iconset_builder/assemblicon/lowlevel/artrdr.py @@ -1,4 +1,3 @@ -import logging import os import enum import subprocess @@ -6,7 +5,8 @@ from typing import Iterable, Iterator from pathlib import Path from dataclasses import dataclass import PIL.Image -from .utils import Artifact, check_vanilla_artifact +from .utils import Artifact +from ..logger import LOWLEVEL_LOGGER as LOGGER class SvgRenderKind(enum.IntEnum): @@ -39,7 +39,7 @@ def render_svg(src: Path, dst: Path, kind: SvgRenderKind, cfg: SvgConfig) -> Non :param kind: The selected SVG render kind. :param cfg: The configuration when rendering. """ - logging.debug( + LOGGER.debug( "Rendering SVG artifact with render %s: %s -> %s", kind.name, src, dst ) @@ -141,7 +141,7 @@ def render_blender(src: Path, dst: Path, temp_script: Path, cfg: BlenderConfig) The caller must make sure that this file is immutable during calling this function. :param cfg: The configuration when rendering. """ - logging.debug("Rendering Blender artifact: %s -> %s", src, dst) + LOGGER.debug("Rendering Blender artifact: %s -> %s", src, dst) if not src.is_file(): raise ValueError( @@ -178,9 +178,19 @@ def render_thumbnail(art: Artifact, hws: Iterable[int]) -> Iterator[Artifact]: :param hws: The int iterable holding all resolution of thumbnails (square thumbnail only) :return: A generator outputing thumbnail one by one with the order of given resolution. """ - check_vanilla_artifact(art) + LOGGER.debug("Rendering thumbnail...") + + # check whether given artifact is square rectangle. + if art.width != art.height: + raise ValueError(f'Thumbnail render only accept square rectangle.') + # scale artifact up to the maximum resolution hws if it is too small. + max_hws = max(hws) + if art.width < max_hws: + art = art.resize((max_hws, max_hws), PIL.Image.Resampling.LANCZOS) + + # render thumbnail for hw in hws: - logging.debug("Rendering thumbnail %dx%d.", hw, hw) + LOGGER.debug("Rendering thumbnail %dx%d.", hw, hw) thumbnail = art.copy() thumbnail.thumbnail((hw, hw), PIL.Image.Resampling.LANCZOS) yield thumbnail @@ -193,9 +203,8 @@ def render_ico(art: Artifact, dst: Path) -> None: :param art: The artifact to be rendered. :param dst: The path to rendered result. """ - logging.debug("Rendering .ICO artifact: %s", dst) + LOGGER.debug("Rendering .ICO artifact: %s", dst) - check_vanilla_artifact(art) sizes = [16, 32, 48, 64, 128, 256] # provide every frame explicitly so that we never depend on # undocumented plugin-side resizing behavior @@ -215,9 +224,8 @@ def render_icns(art: Artifact, dst: Path) -> None: :param art: The artifact to be rendered. :param dst: The path to rendered result. """ - logging.debug("Rendering .ICNS artifact: %s", dst) + LOGGER.debug("Rendering .ICNS artifact: %s", dst) - check_vanilla_artifact(art) # provide every smaller frame explicitly; the 1024x1024 entry is # covered by the art itself frames = list(render_thumbnail(art, (512, 256, 128, 64, 32))) diff --git a/tool/src/aurora_iconset_builder/nostd_icons/__init__.py b/tool/src/aurora_iconset_builder/nostd_icons/__init__.py index 9bee5a6..12324fe 100644 --- a/tool/src/aurora_iconset_builder/nostd_icons/__init__.py +++ b/tool/src/aurora_iconset_builder/nostd_icons/__init__.py @@ -1,10 +1,10 @@ -from ..assemblicon.context import AdvancedContext +from ..assemblicon.highlevel.context import Context from .action_icons import build_action_icons from .mimetype_icons import build_mimetype_icons from .application_icons import build_application_icons -def build_non_standard_icons(ctx: AdvancedContext) -> None: +def register(ctx: Context) -> None: build_mimetype_icons(ctx) build_application_icons(ctx) build_action_icons(ctx) diff --git a/tool/src/aurora_iconset_builder/std_icons/__init__.py b/tool/src/aurora_iconset_builder/std_icons/__init__.py index 0fb8e90..83818a3 100644 --- a/tool/src/aurora_iconset_builder/std_icons/__init__.py +++ b/tool/src/aurora_iconset_builder/std_icons/__init__.py @@ -1,8 +1,8 @@ -from ..assemblicon.context import AdvancedContext +from ..assemblicon.highlevel.context import Context from .action_icons import build_action_icons from .status_icons import build_status_icons -def build_standard_icons(ctx: AdvancedContext) -> None: +def register(ctx: Context) -> None: build_action_icons(ctx) build_status_icons(ctx)