chore: format python codeicons
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 30s
C++ / fmt (pull_request) Successful in 4s
C++ / clang-tidy (pull_request) Failing after 27s
C++ / build (pull_request) Failing after 40s
Rust / fmt (pull_request) Successful in 1m24s
Rust / build (pull_request) Successful in 2m12s
Rust / clippy (pull_request) Successful in 1m52s
Python / verify (pull_request) Successful in 2m24s

This commit is contained in:
2026-08-31 22:34:07 +02:00
parent 0f277de635
commit 24593472e3
+374 -444
View File
@@ -8,452 +8,392 @@ from urllib.error import HTTPError
from urllib.request import urlopen from urllib.request import urlopen
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
DEVICON_VERSION = "v2.17.0" DEVICON_VERSION = "v2.17.0"
ICONS = { ICONS = {
"c": "c", "c": "c",
"cpp": "cplusplus", "cpp": "cplusplus",
"csharp": "csharp", "csharp": "csharp",
"python": "python", "python": "python",
"rust": "rust", "rust": "rust",
"javascript": "javascript", "javascript": "javascript",
"typescript": "typescript", "typescript": "typescript",
"bash": "bash", "bash": "bash",
"zsh": "zsh", "zsh": "zsh",
"powershell": "powershell", "powershell": "powershell",
"cmake": "cmake", "cmake": "cmake",
"lua": "lua", "lua": "lua",
"java": "java", "java": "java",
"kotlin": "kotlin", "kotlin": "kotlin",
"swift": "swift", "swift": "swift",
"go": "go", "go": "go",
"dart": "dart", "dart": "dart",
"php": "php", "php": "php",
"ruby": "ruby", "ruby": "ruby",
"scala": "scala", "scala": "scala",
"haskell": "haskell", "haskell": "haskell",
"elixir": "elixir", "elixir": "elixir",
"erlang": "erlang", "erlang": "erlang",
"clojure": "clojure", "clojure": "clojure",
"r": "r", "r": "r",
"perl": "perl", "perl": "perl",
"zig": "zig", "zig": "zig",
"nim": "nim", "nim": "nim",
"ocaml": "ocaml", "ocaml": "ocaml",
"fsharp": "fsharp", "fsharp": "fsharp",
"visualbasic": "visualbasic", "visualbasic": "visualbasic",
"fortran": "fortran", "fortran": "fortran",
"crystal": "crystal", "crystal": "crystal",
"gleam": "gleam", "gleam": "gleam",
"julia": "julia", "julia": "julia",
"objectivec": "objectivec", "objectivec": "objectivec",
"vala": "vala", "vala": "vala",
"groovy": "groovy", "groovy": "groovy",
"racket": "racket", "racket": "racket",
"haxe": "haxe", "haxe": "haxe",
"purescript": "purescript", "purescript": "purescript",
"delphi": "delphi", "delphi": "delphi",
"coffeescript": "coffeescript", "coffeescript": "coffeescript",
"elm": "elm", "elm": "elm",
"awk": "awk", "awk": "awk",
"matlab": "matlab", "matlab": "matlab",
"solidity": "solidity", "solidity": "solidity",
"wasm": "wasm", "wasm": "wasm",
"vim": "vim", "vim": "vim",
"sql": "sqlite", "sql": "sqlite",
"json": "json", "json": "json",
"yaml": "yaml", "yaml": "yaml",
"xml": "xml", "xml": "xml",
"html": "html5", "html": "html5",
"css": "css3", "css": "css3",
"sass": "sass", "sass": "sass",
"markdown": "markdown", "markdown": "markdown",
"docker": "docker", "docker": "docker",
"latex": "latex", "latex": "latex",
"graphql": "graphql", "graphql": "graphql",
} }
# Markdown / syntax-highlighter aliases -> Devicon icon name.
ALIASES = { ALIASES = {
"cpp": "cpp", "cpp": "cpp",
"cc": "cpp", "cc": "cpp",
"cxx": "cpp", "cxx": "cpp",
"cs": "csharp",
"cs": "csharp", "js": "javascript",
"jsx": "javascript",
"js": "javascript", "ts": "typescript",
"jsx": "javascript", "tsx": "typescript",
"py": "python",
"ts": "typescript", "sh": "bash",
"tsx": "typescript", "shell": "bash",
"ps1": "powershell",
"py": "python", "vb": "visualbasic",
"objective-c": "objectivec",
"sh": "bash", "obj-c": "objectivec",
"shell": "bash", "groovyscript": "groovy",
"pascal": "delphi",
"ps1": "powershell", "coffee": "coffeescript",
"mysql": "sql",
"vb": "visualbasic", "postgres": "sql",
"postgresql": "sql",
"objective-c": "objectivec", "sqlite": "sql",
"obj-c": "objectivec", "yml": "yaml",
"htm": "html",
"groovyscript": "groovy", "scss": "sass",
"md": "markdown",
"pascal": "delphi", "dockerfile": "docker",
"docker-compose": "docker",
"coffee": "coffeescript", "tex": "latex",
"gql": "graphql",
"mysql": "sql",
"postgres": "sql",
"postgresql": "sql",
"sqlite": "sql",
"yml": "yaml",
"htm": "html",
"scss": "sass",
"md": "markdown",
"dockerfile": "docker",
"docker-compose": "docker",
"tex": "latex",
"gql": "graphql",
} }
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
ICON_VARIANTS = ( ICON_VARIANTS = (
"plain", "plain",
"original", "original",
) )
def fetch_svg(devicon_name: str) -> str: def fetch_svg(devicon_name: str) -> str:
for variant in ICON_VARIANTS: for variant in ICON_VARIANTS:
url = ( url = (
f"https://raw.githubusercontent.com/devicons/devicon/" f"https://raw.githubusercontent.com/devicons/devicon/"
f"{DEVICON_VERSION}/icons/{devicon_name}/" f"{DEVICON_VERSION}/icons/{devicon_name}/"
f"{devicon_name}-{variant}.svg" f"{devicon_name}-{variant}.svg"
) )
print(f"Fetching {devicon_name} ({variant}): {url}") print(f"Fetching {devicon_name} ({variant}): {url}")
try: try:
with urlopen(url) as response: with urlopen(url) as response:
return response.read().decode("utf-8") return response.read().decode("utf-8")
except HTTPError as error: except HTTPError as error:
if error.code != 404: if error.code != 404:
raise raise
raise RuntimeError( raise RuntimeError(f"No usable SVG variant found for '{devicon_name}'.")
f"No usable SVG variant found for '{devicon_name}'."
)
def element_to_path(element) -> str: def element_to_path(element) -> str:
"""Convert a basic SVG shape element to equivalent path data.""" """Convert a basic SVG shape element to equivalent path data."""
tag = element.tag.rsplit("}", 1)[-1] tag = element.tag.rsplit("}", 1)[-1]
attrib = element.attrib attrib = element.attrib
if tag == "path": if tag == "path":
return attrib.get("d", "") return attrib.get("d", "")
if tag == "circle": if tag == "circle":
cx = float(attrib["cx"]) cx = float(attrib["cx"])
cy = float(attrib["cy"]) cy = float(attrib["cy"])
r = float(attrib["r"]) r = float(attrib["r"])
return ( return (
f"M {cx - r},{cy} " f"M {cx - r},{cy} "
f"a {r},{r} 0 1,0 {2 * r},0 " f"a {r},{r} 0 1,0 {2 * r},0 "
f"a {r},{r} 0 1,0 {-2 * r},0 Z" f"a {r},{r} 0 1,0 {-2 * r},0 Z"
) )
if tag == "ellipse": if tag == "ellipse":
cx = float(attrib["cx"]) cx = float(attrib["cx"])
cy = float(attrib["cy"]) cy = float(attrib["cy"])
rx = float(attrib["rx"]) rx = float(attrib["rx"])
ry = float(attrib["ry"]) ry = float(attrib["ry"])
return ( return (
f"M {cx - rx},{cy} " f"M {cx - rx},{cy} "
f"a {rx},{ry} 0 1,0 {2 * rx},0 " f"a {rx},{ry} 0 1,0 {2 * rx},0 "
f"a {rx},{ry} 0 1,0 {-2 * rx},0 Z" f"a {rx},{ry} 0 1,0 {-2 * rx},0 Z"
) )
if tag == "rect": if tag == "rect":
x = float(attrib["x"]) x = float(attrib["x"])
y = float(attrib["y"]) y = float(attrib["y"])
width = float(attrib["width"]) width = float(attrib["width"])
height = float(attrib["height"]) height = float(attrib["height"])
return f"M {x},{y} h {width} v {height} h {-width} Z" return f"M {x},{y} h {width} v {height} h {-width} Z"
if tag in ("polygon", "polyline"): if tag in ("polygon", "polyline"):
points = attrib["points"].split() points = attrib["points"].split()
commands = [ commands = [
f"{float(points[i])},{float(points[i + 1])}" f"{float(points[i])},{float(points[i + 1])}"
for i in range(0, len(points) - 1, 2) for i in range(0, len(points) - 1, 2)
] ]
data = "M " + " L ".join(commands) data = "M " + " L ".join(commands)
if tag == "polygon": if tag == "polygon":
data += " Z" data += " Z"
return data return data
if tag == "line": if tag == "line":
return ( return (
f"M {float(attrib['x1'])},{float(attrib['y1'])} " f"M {float(attrib['x1'])},{float(attrib['y1'])} "
f"L {float(attrib['x2'])},{float(attrib['y2'])}" f"L {float(attrib['x2'])},{float(attrib['y2'])}"
) )
return "" return ""
# ---------------------------------------------------------------------------
# SVG path data normalization
#
# Devicon path data crams arc flags against the following coordinate
# (e.g. "a28.78 28.78 0 00-2.65-7.58"), which Qt's PathSvg parser handles
# unreliably. Parse every path and re-emit it with canonical spacing.
# ---------------------------------------------------------------------------
PATH_COMMANDS = "MmLlHhVvCcSsQqTtAaZz" PATH_COMMANDS = "MmLlHhVvCcSsQqTtAaZz"
_NUMBER = re.compile(r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?") _NUMBER = re.compile(r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?")
def _skip_separators(data: str, i: int) -> int: def _skip_separators(data: str, i: int) -> int:
while i < len(data) and data[i] in " \t\r\n,": while i < len(data) and data[i] in " \t\r\n,":
i += 1 i += 1
return i return i
def _read_number(data: str, i: int) -> tuple[str, int]: def _read_number(data: str, i: int) -> tuple[str, int]:
i = _skip_separators(data, i) i = _skip_separators(data, i)
match = _NUMBER.match(data, i) match = _NUMBER.match(data, i)
if not match: if not match:
raise ValueError( raise ValueError(
f"Expected number in path data at {i}: {data[i:i + 24]!r}" f"Expected number in path data at {i}: {data[i : i + 24]!r}"
) )
return match.group(0), match.end() return match.group(0), match.end()
def parse_path(data: str) -> list[tuple[str, list[str]]]: def parse_path(data: str) -> list[tuple[str, list[str]]]:
"""Parse SVG path data into (command, parameters) tuples.""" """Parse SVG path data into (command, parameters) tuples."""
commands: list[tuple[str, list[str]]] = [] commands: list[tuple[str, list[str]]] = []
i, length = 0, len(data) i, length = 0, len(data)
while i < length: while i < length:
i = _skip_separators(data, i) i = _skip_separators(data, i)
if i >= length: if i >= length:
break break
command = data[i] command = data[i]
if command not in PATH_COMMANDS: if command not in PATH_COMMANDS:
raise ValueError( raise ValueError(
f"Unexpected character {command!r} at {i} in {data!r}" f"Unexpected character {command!r} at {i} in {data!r}"
) )
i += 1 i += 1
if command in "Zz": if command in "Zz":
commands.append((command, [])) commands.append((command, []))
continue continue
implicit = command implicit = command
while i < length: while i < length:
i = _skip_separators(data, i) i = _skip_separators(data, i)
if i >= length or data[i] in PATH_COMMANDS: if i >= length or data[i] in PATH_COMMANDS:
break break
if implicit in "Mm": if implicit in "Mm":
x, i = _read_number(data, i) x, i = _read_number(data, i)
y, i = _read_number(data, i) y, i = _read_number(data, i)
commands.append((implicit, [x, y])) commands.append((implicit, [x, y]))
# Subsequent coordinate pairs after a moveto are linetos. implicit = "L" if implicit == "M" else "l"
implicit = "L" if implicit == "M" else "l" elif implicit in "LlTt":
elif implicit in "LlTt": x, i = _read_number(data, i)
x, i = _read_number(data, i) y, i = _read_number(data, i)
y, i = _read_number(data, i) commands.append((implicit, [x, y]))
commands.append((implicit, [x, y])) elif implicit in "Hh":
elif implicit in "Hh": x, i = _read_number(data, i)
x, i = _read_number(data, i) commands.append((implicit, [x]))
commands.append((implicit, [x])) elif implicit in "Vv":
elif implicit in "Vv": y, i = _read_number(data, i)
y, i = _read_number(data, i) commands.append((implicit, [y]))
commands.append((implicit, [y])) elif implicit in "Cc":
elif implicit in "Cc": p = []
# Cubic bezier: x1 y1 x2 y2 x y for _ in range(6):
p = [] value, i = _read_number(data, i)
for _ in range(6): p.append(value)
value, i = _read_number(data, i) commands.append((implicit, p))
p.append(value) elif implicit in "SsQq":
commands.append((implicit, p)) p = []
elif implicit in "SsQq": for _ in range(4):
# Smooth cubic / quadratic: x1 y1 x y value, i = _read_number(data, i)
p = [] p.append(value)
for _ in range(4): commands.append((implicit, p))
value, i = _read_number(data, i) elif implicit in "Aa":
p.append(value) rx, i = _read_number(data, i)
commands.append((implicit, p)) ry, i = _read_number(data, i)
elif implicit in "Aa": rotation, i = _read_number(data, i)
rx, i = _read_number(data, i) i = _skip_separators(data, i)
ry, i = _read_number(data, i) large_arc = data[i]
rotation, i = _read_number(data, i) i += 1
# Arc flags are single digits, possibly crammed against i = _skip_separators(data, i)
# the following coordinate, so read them positionally. sweep = data[i]
i = _skip_separators(data, i) i += 1
large_arc = data[i] if large_arc not in "01" or sweep not in "01":
i += 1 raise ValueError(
i = _skip_separators(data, i) f"Invalid arc flags near {i - 2} in {data!r}"
sweep = data[i] )
i += 1 x, i = _read_number(data, i)
if large_arc not in "01" or sweep not in "01": y, i = _read_number(data, i)
raise ValueError( commands.append(
f"Invalid arc flags near {i - 2} in {data!r}" (implicit, [rx, ry, rotation, large_arc, sweep, x, y])
) )
x, i = _read_number(data, i) else:
y, i = _read_number(data, i) raise ValueError(f"Unhandled path command {implicit!r}")
commands.append(
(implicit, [rx, ry, rotation, large_arc, sweep, x, y])
)
else:
raise ValueError(f"Unhandled path command {implicit!r}")
return commands return commands
def normalize_path(data: str) -> str: def normalize_path(data: str) -> str:
"""Re-emit path data with canonical spacing (Qt PathSvg friendly).""" """Re-emit path data with canonical spacing (Qt PathSvg friendly)."""
return " ".join( return " ".join(
command if not params else command + " " + " ".join(params) command if not params else command + " " + " ".join(params)
for command, params in parse_path(data) for command, params in parse_path(data)
) )
def extract_paths(svg: str) -> str: def extract_paths(svg: str) -> str:
root = ET.fromstring(svg) root = ET.fromstring(svg)
paths = [] paths = []
for element in root.iter(): for element in root.iter():
path = element_to_path(element) path = element_to_path(element)
if not path: if not path:
continue continue
normalized = normalize_path(path) normalized = normalize_path(path)
# Guard against a tokenizer that changes the path it normalizes. if parse_path(normalized) != parse_path(path):
if parse_path(normalized) != parse_path(path): raise RuntimeError(
raise RuntimeError( "Path normalization round-trip mismatch:\n"
"Path normalization round-trip mismatch:\n" f" in : {path[:80]}\n out: {normalized[:80]}"
f" in : {path[:80]}\n out: {normalized[:80]}" )
)
paths.append(normalized) paths.append(normalized)
if not paths: if not paths:
raise RuntimeError("SVG contains no drawable shape elements") raise RuntimeError("SVG contains no drawable shape elements")
# Multiple path elements are valid SVG path data when concatenated. return " ".join(paths)
return " ".join(paths)
def qml_string(value: str) -> str: def qml_string(value: str) -> str:
# JSON string escaping is valid for the string syntax we need in QML. return json.dumps(value, ensure_ascii=False)
return json.dumps(value, ensure_ascii=False)
def qml_identifier(name: str) -> str: def qml_identifier(name: str) -> str:
name = re.sub(r"[^A-Za-z0-9_]", "_", name) name = re.sub(r"[^A-Za-z0-9_]", "_", name)
if name and name[0].isdigit(): if name and name[0].isdigit():
name = "_" + name name = "_" + name
return name return name
# ---------------------------------------------------------------------------
# Generation
# ---------------------------------------------------------------------------
def generate(output: Path) -> None: def generate(output: Path) -> None:
icons = {} icons = {}
for name, devicon_name in ICONS.items(): for name, devicon_name in ICONS.items():
svg = fetch_svg(devicon_name) svg = fetch_svg(devicon_name)
icons[name] = extract_paths(svg) icons[name] = extract_paths(svg)
output.parent.mkdir(parents=True, exist_ok=True) output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8") as file: with output.open("w", encoding="utf-8") as file:
file.write( file.write(
"""pragma Singleton """pragma Singleton
import Quickshell import Quickshell
import QtQuick import QtQuick
Singleton { Singleton {
""" """
) )
# ------------------------------------------------------------------- for icon, path in icons.items():
# Icon path properties identifier = qml_identifier(icon)
# -------------------------------------------------------------------
for icon, path in icons.items(): file.write(
identifier = qml_identifier(icon) f" readonly property string {identifier}: "
f"{qml_string(path)}\n"
)
file.write( file.write("\n")
f" readonly property string {identifier}: "
f"{qml_string(path)}\n"
)
file.write("\n") file.write(
""" function path(language) {
# -------------------------------------------------------------------
# Language -> icon lookup
# -------------------------------------------------------------------
file.write(
""" function path(language) {
const key = language.toLowerCase() const key = language.toLowerCase()
switch (key) { switch (key) {
""" """
) )
for icon in icons: for icon in icons:
identifier = qml_identifier(icon) identifier = qml_identifier(icon)
file.write( file.write(
f' case "{icon}":\n' f' case "{icon}":\n return {identifier}\n'
f" return {identifier}\n" )
)
for alias, icon in ALIASES.items(): for alias, icon in ALIASES.items():
if icon not in icons: if icon not in icons:
raise RuntimeError( raise RuntimeError(
f"Alias '{alias}' points to unavailable icon '{icon}'" f"Alias '{alias}' points to unavailable icon '{icon}'"
) )
identifier = qml_identifier(icon) identifier = qml_identifier(icon)
file.write( file.write(
f' case "{alias}":\n' f' case "{alias}":\n return {identifier}\n'
f" return {identifier}\n" )
)
file.write( file.write(
""" default: """ default:
return "" return ""
} }
} }
@@ -461,112 +401,102 @@ Singleton {
function name(language) { function name(language) {
switch (language.toLowerCase()) { switch (language.toLowerCase()) {
""" """
) )
# Human-readable names can initially be handled here. names = {
# Add/remove these as your codeblock UI evolves. "c": "C",
names = { "cpp": "C++",
"c": "C", "csharp": "C#",
"cpp": "C++", "python": "Python",
"csharp": "C#", "rust": "Rust",
"python": "Python", "javascript": "JavaScript",
"rust": "Rust", "typescript": "TypeScript",
"javascript": "JavaScript", "qml": "QML",
"typescript": "TypeScript", "bash": "Bash",
"qml": "QML", "zsh": "Zsh",
"bash": "Bash", "powershell": "PowerShell",
"zsh": "Zsh", "cmake": "CMake",
"powershell": "PowerShell", "lua": "Lua",
"cmake": "CMake", "java": "Java",
"lua": "Lua", "kotlin": "Kotlin",
"java": "Java", "swift": "Swift",
"kotlin": "Kotlin", "go": "Go",
"swift": "Swift", "dart": "Dart",
"go": "Go", "php": "PHP",
"dart": "Dart", "ruby": "Ruby",
"php": "PHP", "scala": "Scala",
"ruby": "Ruby", "haskell": "Haskell",
"scala": "Scala", "elixir": "Elixir",
"haskell": "Haskell", "erlang": "Erlang",
"elixir": "Elixir", "clojure": "Clojure",
"erlang": "Erlang", "r": "R",
"clojure": "Clojure", "perl": "Perl",
"r": "R", "zig": "Zig",
"perl": "Perl", "nim": "Nim",
"zig": "Zig", "ocaml": "OCaml",
"nim": "Nim", "fsharp": "F#",
"ocaml": "OCaml", "visualbasic": "Visual Basic",
"fsharp": "F#", "fortran": "Fortran",
"visualbasic": "Visual Basic", "crystal": "Crystal",
"fortran": "Fortran", "gleam": "Gleam",
"crystal": "Crystal", "julia": "Julia",
"gleam": "Gleam", "objectivec": "Objective-C",
"julia": "Julia", "vala": "Vala",
"objectivec": "Objective-C", "groovy": "Groovy",
"vala": "Vala", "racket": "Racket",
"groovy": "Groovy", "haxe": "Haxe",
"racket": "Racket", "purescript": "PureScript",
"haxe": "Haxe", "delphi": "Delphi",
"purescript": "PureScript", "coffeescript": "CoffeeScript",
"delphi": "Delphi", "elm": "Elm",
"coffeescript": "CoffeeScript", "awk": "AWK",
"elm": "Elm", "matlab": "MATLAB",
"awk": "AWK", "solidity": "Solidity",
"matlab": "MATLAB", "wasm": "Wasm",
"solidity": "Solidity", "vim": "Vim",
"wasm": "Wasm", "sql": "SQL",
"vim": "Vim", "json": "JSON",
"sql": "SQL", "yaml": "YAML",
"json": "JSON", "xml": "XML",
"yaml": "YAML", "html": "HTML",
"xml": "XML", "css": "CSS",
"html": "HTML", "sass": "Sass",
"css": "CSS", "markdown": "Markdown",
"sass": "Sass", "docker": "Docker",
"markdown": "Markdown", "latex": "LaTeX",
"docker": "Docker", "graphql": "GraphQL",
"latex": "LaTeX", }
"graphql": "GraphQL",
}
for icon in icons: for icon in icons:
if icon not in names: if icon not in names:
continue continue
name = names[icon] name = names[icon]
file.write( file.write(f' case "{icon}":\n return "{name}"\n')
f' case "{icon}":\n'
f' return "{name}"\n'
)
for alias, icon in ALIASES.items(): for alias, icon in ALIASES.items():
if icon not in names: if icon not in names:
continue continue
file.write( file.write(
f' case "{alias}":\n' f' case "{alias}":\n return "{names[icon]}"\n'
f' return "{names[icon]}"\n' )
)
file.write( file.write(
""" default: """ default:
return language return language
} }
} }
} }
""" """
) )
print(f"Generated {output}") print(f"Generated {output}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
if len(sys.argv) != 2: if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} OUTPUT.qml", file=sys.stderr) print(f"Usage: {sys.argv[0]} OUTPUT.qml", file=sys.stderr)
sys.exit(1) sys.exit(1)
generate(Path(sys.argv[1])) generate(Path(sys.argv[1]))