Merge pull request #2 from vokhactri/refactor/add-python-type-hints
Add type hints to KDE Python build scripts
This commit was merged in pull request #2.
This commit is contained in:
3 files changed
+49
-41
No files matched your search
@@ -30,7 +30,7 @@ COLS, ROWS = 20, 10
|
|||||||
KONSOLE_CELL = 8, 17
|
KONSOLE_CELL = 8, 17
|
||||||
|
|
||||||
|
|
||||||
def encode(png, dst):
|
def encode(png: bytes, dst: Path) -> None:
|
||||||
data = base64.b64encode(png)
|
data = base64.b64encode(png)
|
||||||
chunks = [data[i : i + 4096] for i in range(0, len(data), 4096)]
|
chunks = [data[i : i + 4096] for i in range(0, len(data), 4096)]
|
||||||
out = bytearray()
|
out = bytearray()
|
||||||
@@ -44,7 +44,7 @@ def encode(png, dst):
|
|||||||
dst.write_bytes(out)
|
dst.write_bytes(out)
|
||||||
|
|
||||||
|
|
||||||
def resize(width, height):
|
def resize(width: int, height: int) -> bytes:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
out = Path(tmp) / "out.png"
|
out = Path(tmp) / "out.png"
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
@@ -14,11 +15,11 @@ COMPONENT = "org.kde.spectacle.desktop"
|
|||||||
ACCEL = "/component/org_kde_spectacle_desktop"
|
ACCEL = "/component/org_kde_spectacle_desktop"
|
||||||
|
|
||||||
|
|
||||||
def run(*args, **kwargs):
|
def run(*args: str, **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
||||||
return subprocess.run(args, check=True, text=True, **kwargs)
|
return subprocess.run(args, check=True, text=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def accel(path, method, *args):
|
def accel(path: str, method: str, *args: str) -> str:
|
||||||
return run(
|
return run(
|
||||||
"gdbus", "call", "--session", "--dest", "org.kde.kglobalaccel",
|
"gdbus", "call", "--session", "--dest", "org.kde.kglobalaccel",
|
||||||
"--object-path", path, "--method", method, *args,
|
"--object-path", path, "--method", method, *args,
|
||||||
@@ -26,10 +27,10 @@ def accel(path, method, *args):
|
|||||||
).stdout
|
).stdout
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
for name in ("spectaclerc", "klipperrc"):
|
for name in ("spectaclerc", "klipperrc"):
|
||||||
config = configparser.ConfigParser(interpolation=None)
|
config = configparser.ConfigParser(interpolation=None)
|
||||||
config.optionxform = str
|
config.optionxform = str # type: ignore[method-assign,assignment]
|
||||||
config.read(HERE / "settings" / name)
|
config.read(HERE / "settings" / name)
|
||||||
for group in config.sections():
|
for group in config.sections():
|
||||||
for key, value in config[group].items():
|
for key, value in config[group].items():
|
||||||
@@ -38,7 +39,7 @@ def main():
|
|||||||
|
|
||||||
# T3's locale and XDG_DATA_DIRS can differ from the running KDE session.
|
# T3's locale and XDG_DATA_DIRS can differ from the running KDE session.
|
||||||
# Build the cache KDE actually reads, rather than an app-specific cache.
|
# Build the cache KDE actually reads, rather than an app-specific cache.
|
||||||
session = dict(
|
session: dict[str, str] = dict(
|
||||||
line.split("=", 1) for line in
|
line.split("=", 1) for line in
|
||||||
run("systemctl", "--user", "show-environment", capture_output=True)
|
run("systemctl", "--user", "show-environment", capture_output=True)
|
||||||
.stdout.splitlines() if "=" in line
|
.stdout.splitlines() if "=" in line
|
||||||
@@ -61,7 +62,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
output = accel(ACCEL, "org.kde.kglobalaccel.Component.allShortcutInfos")
|
output = accel(ACCEL, "org.kde.kglobalaccel.Component.allShortcutInfos")
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
shortcuts = [] # Component has not been registered yet.
|
shortcuts: list[Any] = [] # Component has not been registered yet.
|
||||||
else:
|
else:
|
||||||
shortcuts = ast.literal_eval(re.sub(r"@a[is] ", "", output))[0]
|
shortcuts = ast.literal_eval(re.sub(r"@a[is] ", "", output))[0]
|
||||||
accel(ACCEL, "org.kde.kglobalaccel.Component.cleanUp")
|
accel(ACCEL, "org.kde.kglobalaccel.Component.cleanUp")
|
||||||
|
|||||||
+40
-33
@@ -18,6 +18,7 @@ import re
|
|||||||
import struct
|
import struct
|
||||||
import zipfile
|
import zipfile
|
||||||
import zlib
|
import zlib
|
||||||
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
@@ -40,6 +41,11 @@ VSCODE_UI = VSCODE / "base/2026-dark.json"
|
|||||||
# 0.1.4). The editor uses its token colors as they are.
|
# 0.1.4). The editor uses its token colors as they are.
|
||||||
VSCODE_SYNTAX = VSCODE / "base/pierre-dark.json"
|
VSCODE_SYNTAX = VSCODE / "base/pierre-dark.json"
|
||||||
|
|
||||||
|
# An RGB or RGBA color, 0-255 per channel.
|
||||||
|
type Color = tuple[int, ...]
|
||||||
|
# Looks up a color in the scheme by group and key.
|
||||||
|
type ColorFn = Callable[[str, str], Color]
|
||||||
|
|
||||||
WHITE = (255, 255, 255)
|
WHITE = (255, 255, 255)
|
||||||
|
|
||||||
# Apple systemCyan; the KDE scheme has no cyan role.
|
# Apple systemCyan; the KDE scheme has no cyan role.
|
||||||
@@ -53,27 +59,27 @@ YELLOW = (255, 255, 0)
|
|||||||
INACTIVE_TAB_MIX = 0.12
|
INACTIVE_TAB_MIX = 0.12
|
||||||
|
|
||||||
|
|
||||||
def load():
|
def load() -> ColorFn:
|
||||||
cp = configparser.ConfigParser(interpolation=None, strict=False)
|
cp = configparser.ConfigParser(interpolation=None, strict=False)
|
||||||
cp.optionxform = str
|
cp.optionxform = str # type: ignore[method-assign,assignment]
|
||||||
cp.read(SCHEME)
|
cp.read(SCHEME)
|
||||||
|
|
||||||
def color(group, key):
|
def color(group: str, key: str) -> Color:
|
||||||
return tuple(int(v) for v in cp[f"Colors:{group}"][key].split(","))
|
return tuple(int(v) for v in cp[f"Colors:{group}"][key].split(","))
|
||||||
|
|
||||||
return color
|
return color
|
||||||
|
|
||||||
|
|
||||||
def mix(a, b, t):
|
def mix(a: Sequence[int], b: Sequence[int], t: float) -> Color:
|
||||||
return tuple(round(x + (y - x) * t) for x, y in zip(a, b))
|
return tuple(round(x + (y - x) * t) for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
def hexc(c):
|
def hexc(c: Sequence[int]) -> str:
|
||||||
"""#rrggbb, or #rrggbbaa when c carries an alpha."""
|
"""#rrggbb, or #rrggbbaa when c carries an alpha."""
|
||||||
return "#" + "".join(f"{v:02x}" for v in c)
|
return "#" + "".join(f"{v:02x}" for v in c)
|
||||||
|
|
||||||
|
|
||||||
def kwin_compensate(c):
|
def kwin_compensate(c: Sequence[int]) -> list[int]:
|
||||||
"""Pre-distort a color for Chromium on KWin.
|
"""Pre-distort a color for Chromium on KWin.
|
||||||
|
|
||||||
Chromium tags its surfaces as sRGB, KWin shows them with a gamma 2.2
|
Chromium tags its surfaces as sRGB, KWin shows them with a gamma 2.2
|
||||||
@@ -81,15 +87,15 @@ def kwin_compensate(c):
|
|||||||
with the inverse makes Helium land on the same values as native apps.
|
with the inverse makes Helium land on the same values as native apps.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def ch(v):
|
def ch(v: int) -> int:
|
||||||
lin = (v / 255) ** 2.2
|
lin: float = (v / 255) ** 2.2
|
||||||
enc = 12.92 * lin if lin <= 0.0031308 else 1.055 * lin ** (1 / 2.4) - 0.055
|
enc = 12.92 * lin if lin <= 0.0031308 else 1.055 * lin ** (1 / 2.4) - 0.055
|
||||||
return round(enc * 255)
|
return round(enc * 255)
|
||||||
|
|
||||||
return [ch(v) for v in c]
|
return [ch(v) for v in c]
|
||||||
|
|
||||||
|
|
||||||
def helium(color):
|
def helium(color: ColorFn) -> dict[str, list[int]]:
|
||||||
bar = color("Header", "BackgroundNormal")
|
bar = color("Header", "BackgroundNormal")
|
||||||
text = color("Window", "ForegroundNormal")
|
text = color("Window", "ForegroundNormal")
|
||||||
view = color("View", "BackgroundNormal")
|
view = color("View", "BackgroundNormal")
|
||||||
@@ -123,7 +129,7 @@ def helium(color):
|
|||||||
return {k: kwin_compensate(v) for k, v in colors.items()}
|
return {k: kwin_compensate(v) for k, v in colors.items()}
|
||||||
|
|
||||||
|
|
||||||
def ansi(color):
|
def ansi(color: ColorFn) -> list[Color]:
|
||||||
"""The 16 colors for the VS Code terminal."""
|
"""The 16 colors for the VS Code terminal."""
|
||||||
bg = color("View", "BackgroundNormal")
|
bg = color("View", "BackgroundNormal")
|
||||||
fg = color("View", "ForegroundNormal")
|
fg = color("View", "ForegroundNormal")
|
||||||
@@ -145,7 +151,7 @@ def ansi(color):
|
|||||||
return normal + bright
|
return normal + bright
|
||||||
|
|
||||||
|
|
||||||
def t3(color):
|
def t3(color: ColorFn) -> dict[str, str]:
|
||||||
"""Every color slot of a T3 Code theme, mapped onto a scheme role."""
|
"""Every color slot of a T3 Code theme, mapped onto a scheme role."""
|
||||||
window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal")
|
window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal")
|
||||||
view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal")
|
view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal")
|
||||||
@@ -227,7 +233,7 @@ def t3(color):
|
|||||||
return {k: hexc(kwin_compensate(v)) for k, v in colors.items()}
|
return {k: hexc(kwin_compensate(v)) for k, v in colors.items()}
|
||||||
|
|
||||||
|
|
||||||
def telegram(color):
|
def telegram(color: ColorFn) -> dict[str, Color]:
|
||||||
"""Telegram keys mapped onto a scheme role; the rest come from night."""
|
"""Telegram keys mapped onto a scheme role; the rest come from night."""
|
||||||
window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal")
|
window, window_text = color("Window", "BackgroundNormal"), color("Window", "ForegroundNormal")
|
||||||
view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal")
|
view, view_text = color("View", "BackgroundNormal"), color("View", "ForegroundNormal")
|
||||||
@@ -360,25 +366,25 @@ def telegram(color):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def luma(c):
|
def luma(c: Sequence[int]) -> float:
|
||||||
return 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
return 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||||
|
|
||||||
|
|
||||||
def interp(points, x):
|
def interp(points: Sequence[tuple[float, float]], x: float) -> float:
|
||||||
for (x0, y0), (x1, y1) in zip(points, points[1:]):
|
for (x0, y0), (x1, y1) in zip(points, points[1:]):
|
||||||
if x <= x1:
|
if x <= x1:
|
||||||
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
|
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
|
||||||
return points[-1][1]
|
return points[-1][1]
|
||||||
|
|
||||||
|
|
||||||
def parse_hex(v):
|
def parse_hex(v: str) -> Color:
|
||||||
h = v[1:]
|
h = v[1:]
|
||||||
if len(h) in (3, 4):
|
if len(h) in (3, 4):
|
||||||
h = "".join(ch * 2 for ch in h)
|
h = "".join(ch * 2 for ch in h)
|
||||||
return tuple(int(h[i:i + 2], 16) for i in range(0, len(h), 2))
|
return tuple(int(h[i:i + 2], 16) for i in range(0, len(h), 2))
|
||||||
|
|
||||||
|
|
||||||
def telegram_theme(color):
|
def telegram_theme(color: ColorFn) -> str:
|
||||||
"""The night theme, recolored onto the scheme.
|
"""The night theme, recolored onto the scheme.
|
||||||
|
|
||||||
Night tints everything blue. Its tinted grays become scheme grays of
|
Night tints everything blue. Its tinted grays become scheme grays of
|
||||||
@@ -399,7 +405,7 @@ def telegram_theme(color):
|
|||||||
})
|
})
|
||||||
accent_h, _, accent_s = colorsys.rgb_to_hls(*(v / 255 for v in color("Selection", "BackgroundNormal")))
|
accent_h, _, accent_s = colorsys.rgb_to_hls(*(v / 255 for v in color("Selection", "BackgroundNormal")))
|
||||||
|
|
||||||
def recolor(c):
|
def recolor(c: Color) -> Color:
|
||||||
rgb, alpha = c[:3], c[3:]
|
rgb, alpha = c[:3], c[3:]
|
||||||
chroma = max(rgb) - min(rgb)
|
chroma = max(rgb) - min(rgb)
|
||||||
h, l, _ = colorsys.rgb_to_hls(*(v / 255 for v in rgb))
|
h, l, _ = colorsys.rgb_to_hls(*(v / 255 for v in rgb))
|
||||||
@@ -411,7 +417,7 @@ def telegram_theme(color):
|
|||||||
return (*(round(v * 255) for v in colorsys.hls_to_rgb(accent_h, l, accent_s)), *alpha)
|
return (*(round(v * 255) for v in colorsys.hls_to_rgb(accent_h, l, accent_s)), *alpha)
|
||||||
|
|
||||||
pinned = telegram(color)
|
pinned = telegram(color)
|
||||||
seen = set()
|
seen: set[str] = set()
|
||||||
out = [f"// Generated by kde/theme/build.py from {SCHEME.name}"]
|
out = [f"// Generated by kde/theme/build.py from {SCHEME.name}"]
|
||||||
for line in base.splitlines():
|
for line in base.splitlines():
|
||||||
m = line_re.match(line)
|
m = line_re.match(line)
|
||||||
@@ -429,20 +435,21 @@ def telegram_theme(color):
|
|||||||
return "\n".join(out) + "\n"
|
return "\n".join(out) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def vscode_base_colors():
|
def vscode_base_colors() -> dict[str, Color]:
|
||||||
"""Dark 2026's colors with its include chain resolved."""
|
"""Dark 2026's colors with its include chain resolved."""
|
||||||
chain, path = [], VSCODE_UI
|
chain: list[dict[str, str]] = []
|
||||||
|
path: Path | None = VSCODE_UI
|
||||||
while path:
|
while path:
|
||||||
theme = json.loads(path.read_text())
|
theme = json.loads(path.read_text())
|
||||||
chain.append(theme.get("colors", {}))
|
chain.append(theme.get("colors", {}))
|
||||||
path = path.parent / theme["include"] if "include" in theme else None
|
path = path.parent / theme["include"] if "include" in theme else None
|
||||||
colors = {}
|
colors: dict[str, str] = {}
|
||||||
for layer in reversed(chain):
|
for layer in reversed(chain):
|
||||||
colors.update(layer)
|
colors.update(layer)
|
||||||
return {k: parse_hex(v) for k, v in colors.items()}
|
return {k: parse_hex(v) for k, v in colors.items()}
|
||||||
|
|
||||||
|
|
||||||
def vscode(color):
|
def vscode(color: ColorFn) -> dict[str, str]:
|
||||||
"""Workbench colors: Dark 2026, recolored onto the scheme.
|
"""Workbench colors: Dark 2026, recolored onto the scheme.
|
||||||
|
|
||||||
Dark 2026 lays out its grays the way the scheme does: the chrome lighter
|
Dark 2026 lays out its grays the way the scheme does: the chrome lighter
|
||||||
@@ -475,11 +482,11 @@ def vscode(color):
|
|||||||
})
|
})
|
||||||
assert all(a[1] <= b[1] for a, b in zip(grays, grays[1:])), grays
|
assert all(a[1] <= b[1] for a, b in zip(grays, grays[1:])), grays
|
||||||
|
|
||||||
def hls(c):
|
def hls(c: Sequence[int]) -> tuple[float, float, float]:
|
||||||
return colorsys.rgb_to_hls(*(v / 255 for v in c))
|
return colorsys.rgb_to_hls(*(v / 255 for v in c))
|
||||||
|
|
||||||
# (hue range in degrees, scheme color whose hue it takes)
|
# (hue range in degrees, scheme color whose hue it takes)
|
||||||
families = [
|
families: list[tuple[tuple[int, int], Color]] = [
|
||||||
((0, 15), negative),
|
((0, 15), negative),
|
||||||
((15, 70), neutral),
|
((15, 70), neutral),
|
||||||
((70, 170), positive),
|
((70, 170), positive),
|
||||||
@@ -488,7 +495,7 @@ def vscode(color):
|
|||||||
((330, 360), negative),
|
((330, 360), negative),
|
||||||
]
|
]
|
||||||
|
|
||||||
def recolor(c):
|
def recolor(c: Color) -> Color:
|
||||||
rgb, alpha = c[:3], c[3:]
|
rgb, alpha = c[:3], c[3:]
|
||||||
if max(rgb) - min(rgb) <= 10:
|
if max(rgb) - min(rgb) <= 10:
|
||||||
g = round(interp(grays, luma(rgb)))
|
g = round(interp(grays, luma(rgb)))
|
||||||
@@ -497,7 +504,7 @@ def vscode(color):
|
|||||||
target = next(t for (lo, hi), t in families if lo <= h * 360 < hi)
|
target = next(t for (lo, hi), t in families if lo <= h * 360 < hi)
|
||||||
return (*(round(v * 255) for v in colorsys.hls_to_rgb(hls(target)[0], l, sat)), *alpha)
|
return (*(round(v * 255) for v in colorsys.hls_to_rgb(hls(target)[0], l, sat)), *alpha)
|
||||||
|
|
||||||
pins = {
|
pins: dict[str, Color] = {
|
||||||
"editor.background": view,
|
"editor.background": view,
|
||||||
"editor.foreground": view_text,
|
"editor.foreground": view_text,
|
||||||
"titleBar.inactiveBackground": color("Header][Inactive", "BackgroundNormal"),
|
"titleBar.inactiveBackground": color("Header][Inactive", "BackgroundNormal"),
|
||||||
@@ -527,7 +534,7 @@ def vscode(color):
|
|||||||
return {k: hexc(kwin_compensate(c[:3]) + list(c[3:])) for k, c in sorted(colors.items())}
|
return {k: hexc(kwin_compensate(c[:3]) + list(c[3:])) for k, c in sorted(colors.items())}
|
||||||
|
|
||||||
|
|
||||||
def vscode_files(color):
|
def vscode_files(color: ColorFn) -> dict[str, str]:
|
||||||
"""The extension files, keyed by their path inside it."""
|
"""The extension files, keyed by their path inside it."""
|
||||||
name = "Darwin Breeze Dark"
|
name = "Darwin Breeze Dark"
|
||||||
# Syntax colors as T3 Code shows them. T3 Code doesn't compensate them
|
# Syntax colors as T3 Code shows them. T3 Code doesn't compensate them
|
||||||
@@ -560,7 +567,7 @@ def vscode_files(color):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def vsix(files):
|
def vsix(files: Mapping[str, str]) -> bytes:
|
||||||
"""Pack the extension the way vsce does, for code --install-extension."""
|
"""Pack the extension the way vsce does, for code --install-extension."""
|
||||||
manifest = """<?xml version="1.0" encoding="utf-8"?>
|
manifest = """<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
|
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
|
||||||
@@ -592,10 +599,10 @@ def vsix(files):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def solid_png(c):
|
def solid_png(c: Sequence[int]) -> bytes:
|
||||||
"""A 1x1 PNG; Telegram stretches it over the chat as a plain background."""
|
"""A 1x1 PNG; Telegram stretches it over the chat as a plain background."""
|
||||||
|
|
||||||
def chunk(kind, data):
|
def chunk(kind: bytes, data: bytes) -> bytes:
|
||||||
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
|
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -606,7 +613,7 @@ def solid_png(c):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def zip_bytes(files):
|
def zip_bytes(files: Mapping[str, str | bytes]) -> bytes:
|
||||||
"""A zip with fixed timestamps, so an unchanged theme rebuilds byte-identical."""
|
"""A zip with fixed timestamps, so an unchanged theme rebuilds byte-identical."""
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
@@ -618,7 +625,7 @@ def zip_bytes(files):
|
|||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def write(path, data):
|
def write(path: Path, data: str | bytes) -> bool:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.encode()
|
data = data.encode()
|
||||||
@@ -629,7 +636,7 @@ def write(path, data):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main() -> None:
|
||||||
color = load()
|
color = load()
|
||||||
manifest = {
|
manifest = {
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
|
|||||||
Reference in new issue
Block a user