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
|
||||
|
||||
|
||||
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 +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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user