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