Add type hints to KDE Python build scripts #2

Merged
vokhactri merged 1 commits from refactor/add-python-type-hints into main 2026-11-02 15:18:44 +07:00
3 changed files with 49 additions and 41 deletions
Showing only changes of commit 3d1011e17c - Show all commits

No files matched your search

+2 -2
View File
@@ -30,7 +30,7 @@ COLS, ROWS = 20, 10
KONSOLE_CELL = 8, 17
def encode(png, dst):
def encode(png: bytes, dst: Path) -> None:
data = base64.b64encode(png)
chunks = [data[i : i + 4096] for i in range(0, len(data), 4096)]
out = bytearray()
@@ -44,7 +44,7 @@ def encode(png, dst):
dst.write_bytes(out)
def resize(width, height):
def resize(width: int, height: int) -> bytes:
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "out.png"
subprocess.run(
+7 -6
View File
@@ -7,6 +7,7 @@ import os
from pathlib import Path
import re
import subprocess
from typing import Any
HERE = Path(__file__).resolve().parent
@@ -14,11 +15,11 @@ 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)
def accel(path, method, *args):
def accel(path: str, method: str, *args: str) -> str:
return run(
"gdbus", "call", "--session", "--dest", "org.kde.kglobalaccel",
"--object-path", path, "--method", method, *args,
@@ -26,10 +27,10 @@ def accel(path, method, *args):
).stdout
def main():
def main() -> None:
for name in ("spectaclerc", "klipperrc"):
config = configparser.ConfigParser(interpolation=None)
config.optionxform = str
config.optionxform = str # type: ignore[method-assign,assignment]
config.read(HERE / "settings" / name)
for group in config.sections():
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.
# 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
run("systemctl", "--user", "show-environment", capture_output=True)
.stdout.splitlines() if "=" in line
@@ -61,7 +62,7 @@ def main():
try:
output = accel(ACCEL, "org.kde.kglobalaccel.Component.allShortcutInfos")
except subprocess.CalledProcessError:
shortcuts = [] # Component has not been registered yet.
shortcuts: list[Any] = [] # Component has not been registered yet.
else:
shortcuts = ast.literal_eval(re.sub(r"@a[is] ", "", output))[0]
accel(ACCEL, "org.kde.kglobalaccel.Component.cleanUp")
+40 -33
View File
@@ -18,6 +18,7 @@ import re
import struct
import zipfile
import zlib
from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
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.
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)
# Apple systemCyan; the KDE scheme has no cyan role.
@@ -53,27 +59,27 @@ YELLOW = (255, 255, 0)
INACTIVE_TAB_MIX = 0.12
def load():
def load() -> ColorFn:
cp = configparser.ConfigParser(interpolation=None, strict=False)
cp.optionxform = str
cp.optionxform = str # type: ignore[method-assign,assignment]
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 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))
def hexc(c):
def hexc(c: Sequence[int]) -> str:
"""#rrggbb, or #rrggbbaa when c carries an alpha."""
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.
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.
"""
def ch(v):
lin = (v / 255) ** 2.2
def ch(v: int) -> int:
lin: float = (v / 255) ** 2.2
enc = 12.92 * lin if lin <= 0.0031308 else 1.055 * lin ** (1 / 2.4) - 0.055
return round(enc * 255)
return [ch(v) for v in c]
def helium(color):
def helium(color: ColorFn) -> dict[str, list[int]]:
bar = color("Header", "BackgroundNormal")
text = color("Window", "ForegroundNormal")
view = color("View", "BackgroundNormal")
@@ -123,7 +129,7 @@ def helium(color):
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."""
bg = color("View", "BackgroundNormal")
fg = color("View", "ForegroundNormal")
@@ -145,7 +151,7 @@ def ansi(color):
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."""
window, window_text = color("Window", "BackgroundNormal"), color("Window", "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()}
def telegram(color):
def telegram(color: ColorFn) -> dict[str, Color]:
"""Telegram keys mapped onto a scheme role; the rest come from night."""
window, window_text = color("Window", "BackgroundNormal"), color("Window", "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]
def interp(points, x):
def interp(points: Sequence[tuple[float, float]], x: float) -> float:
for (x0, y0), (x1, y1) in zip(points, points[1:]):
if x <= x1:
return y0 + (y1 - y0) * (x - x0) / (x1 - x0)
return points[-1][1]
def parse_hex(v):
def parse_hex(v: str) -> Color:
h = v[1:]
if len(h) in (3, 4):
h = "".join(ch * 2 for ch in h)
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.
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")))
def recolor(c):
def recolor(c: Color) -> Color:
rgb, alpha = c[:3], c[3:]
chroma = max(rgb) - min(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)
pinned = telegram(color)
seen = set()
seen: set[str] = set()
out = [f"// Generated by kde/theme/build.py from {SCHEME.name}"]
for line in base.splitlines():
m = line_re.match(line)
@@ -429,20 +435,21 @@ def telegram_theme(color):
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."""
chain, path = [], VSCODE_UI
chain: list[dict[str, str]] = []
path: Path | None = VSCODE_UI
while path:
theme = json.loads(path.read_text())
chain.append(theme.get("colors", {}))
path = path.parent / theme["include"] if "include" in theme else None
colors = {}
colors: dict[str, str] = {}
for layer in reversed(chain):
colors.update(layer)
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.
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
def hls(c):
def hls(c: Sequence[int]) -> tuple[float, float, float]:
return colorsys.rgb_to_hls(*(v / 255 for v in c))
# (hue range in degrees, scheme color whose hue it takes)
families = [
families: list[tuple[tuple[int, int], Color]] = [
((0, 15), negative),
((15, 70), neutral),
((70, 170), positive),
@@ -488,7 +495,7 @@ def vscode(color):
((330, 360), negative),
]
def recolor(c):
def recolor(c: Color) -> Color:
rgb, alpha = c[:3], c[3:]
if max(rgb) - min(rgb) <= 10:
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)
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.foreground": view_text,
"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())}
def vscode_files(color):
def vscode_files(color: ColorFn) -> dict[str, str]:
"""The extension files, keyed by their path inside it."""
name = "Darwin Breeze Dark"
# 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."""
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">
@@ -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."""
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 (
@@ -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."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as z:
@@ -618,7 +625,7 @@ def zip_bytes(files):
return buf.getvalue()
def write(path, data):
def write(path: Path, data: str | bytes) -> bool:
path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(data, str):
data = data.encode()
@@ -629,7 +636,7 @@ def write(path, data):
return True
def main():
def main() -> None:
color = load()
manifest = {
"manifest_version": 3,