#!/usr/bin/env python3
"""Generates icons/*.svg and icons/theme.json of the Material Icons plugin.
Flat, original icons on the Material colour palette: language files are
coloured badges with a short label, well-known files get glyphs, folders are
tinted by role. Edit the tables below and run `python3 generate.py`, then
`openidea-plugin check .`. Not part of the package (plugin.toml does not
reference it), so it is not installed.
"""
import json
import pathlib
HERE = pathlib.Path(__file__).resolve().parent
OUT = HERE / "icons"
FONT = "system-ui, -apple-system, Segoe UI, Roboto, sans-serif"
def svg(body: str) -> str:
return f'\n'
def badge(color: str, label: str, fg: str = "#fff") -> str:
size = {1: 9.5, 2: 7.4, 3: 5.6}[len(label)]
y = 8 + size * 0.36
label = label.replace("&", "&").replace("<", "<").replace(">", ">")
return svg(
f''
f'{label}'
)
def doc(color: str, inner: str = "") -> str:
return svg(
f''
f'{inner}'
)
def folder(color: str, open_: bool = False) -> str:
if open_:
return svg(
f''
f''
)
return svg(
f''
)
def stroke(d: str, color: str = "#fff", width: float = 1.4) -> str:
return (
f''
)
def gear(color: str) -> str:
teeth = "".join(
f''
for a in range(0, 360, 45)
)
return svg(f'{teeth}')
def atom(color: str) -> str:
rings = "".join(
f'' for a in (0, 60, 120)
)
return svg(f'{rings}')
def package(color: str) -> str:
return svg(
f''
''
''
)
GLYPHS = {
"file": doc("#90a4ae"),
"text": doc("#90a4ae", stroke("M5 7.5h6M5 10h6M5 12.5h4", width=1.1)),
"markdown": svg(
''
+ stroke("M3 10.5v-5l2 2.5 2-2.5v5", width=1.3)
+ stroke("M11 5.5v5M9.2 8.8l1.8 1.8 1.8-1.8", width=1.3)
),
"image": svg(
''
''
''
),
"archive": svg(
''
''
''
),
"lock": svg(
''
+ stroke("M5.5 7V5a2.5 2.5 0 0 1 5 0v2", "#90a4ae", 1.8)
+ ''
),
"git": svg(
''
+ stroke("M6.3 5v6M6.3 6.4c0 1.8 3.7.4 3.7 2.1", width=1.1)
+ ''
),
"settings": gear("#78909c"),
"env": gear("#fbc02d"),
"readme": svg(
''
''
),
"license": svg(
''
+ stroke("M3.6 4.6h6.4M3.6 6.8h4", "#fff", 1.1)
+ ''
),
"shell": svg(
''
+ stroke("M4 5.5l2.5 2.5L4 10.5", width=1.5)
+ stroke("M8 10.8h4", width=1.5)
),
"database": svg(
''
''
+ stroke("M2.5 7.5c0 1.1 2.5 2 5.5 2s5.5-.9 5.5-2", "#ffe082", 0.9)
),
"html": svg('' + stroke("M6 5L3.5 8 6 11M10 5l2.5 3L10 11")),
"xml": svg('' + stroke("M5.5 5L3 8l2.5 3M10.5 5L13 8l-2.5 3M9 4.5l-2 7", width=1.2)),
"json": badge("#fbc02d", "{}", "#5d4037"),
"react": atom("#00bcd4"),
"react-ts": atom("#0288d1"),
"docker": svg(
''
''
''
''
''
),
"npm": package("#cb3837"),
"cargo": package("#ff7043"),
"gomod": package("#00acc1"),
"pnpm": package("#f9ad00"),
"test": svg(
'' + stroke("M4.2 8.2l2.4 2.4 5.2-5.2", width=1.7)
),
"vite": badge("#9575cd", "V"),
"tsconfig": badge("#0288d1", "TS", "#bbdefb"),
"makefile": badge("#ef5350", "M"),
"editorconfig": gear("#90a4ae"),
"font": badge("#ef5350", "Aa"),
"pdf": badge("#e53935", "PDF"),
"svg": badge("#ffb300", "SVG", "#4e342e"),
"audio": svg(''),
"video": svg(''),
}
# extension → (icon id, colour, label[, label colour])
LANGUAGES = {
"rust": ("#ff7043", "RS", ["rs"]),
"typescript": ("#0288d1", "TS", ["ts", "mts", "cts"]),
"typescript-def": ("#0277bd", "D", ["d.ts", "d.mts", "d.cts"]),
"javascript": ("#fbc02d", "JS", ["js", "mjs", "cjs"], "#3e2723"),
"python": ("#3572a5", "PY", ["py", "pyi", "pyw"], "#ffd54f"),
"go": ("#00acc1", "GO", ["go"]),
"java": ("#e53935", "J", ["java"]),
"class": ("#8d6e63", "CL", ["class", "jar"]),
"kotlin": ("#7e57c2", "KT", ["kt", "kts"]),
"groovy": ("#4db6ac", "GR", ["groovy", "gradle"]),
"scala": ("#d32f2f", "SC", ["scala", "sc", "sbt"]),
"c": ("#5c6bc0", "C", ["c"]),
"cpp": ("#1e88e5", "C++", ["cpp", "cc", "cxx", "c++"]),
"header": ("#9575cd", "H", ["h", "hpp", "hh", "hxx", "inl"]),
"csharp": ("#388e3c", "C#", ["cs", "csx"]),
"fsharp": ("#378bba", "F#", ["fs", "fsi", "fsx"]),
"swift": ("#ff5722", "SW", ["swift"]),
"objc": ("#607d8b", "OC", ["m", "mm"]),
"ruby": ("#c62828", "RB", ["rb", "erb", "rake", "gemspec"]),
"php": ("#7986cb", "PHP", ["php"]),
"perl": ("#5c6bc0", "PL", ["pl", "pm"]),
"lua": ("#3949ab", "LUA", ["lua"]),
"r": ("#1976d2", "R", ["r", "rmd"]),
"julia": ("#9558b2", "JL", ["jl"]),
"dart": ("#40c4ff", "D", ["dart"], "#01579b"),
"elixir": ("#7e57c2", "EX", ["ex", "exs", "heex"]),
"erlang": ("#c2185b", "ER", ["erl", "hrl"]),
"haskell": ("#7e57c2", "HS", ["hs", "lhs"]),
"ocaml": ("#ef6c00", "ML", ["ml", "mli"]),
"clojure": ("#64dd17", "CLJ", ["clj", "cljs", "cljc", "edn"], "#1b5e20"),
"zig": ("#f9a825", "Z", ["zig", "zon"], "#3e2723"),
"nim": ("#ffc107", "NIM", ["nim"], "#3e2723"),
"solidity": ("#546e7a", "SOL", ["sol"]),
"vue": ("#41b883", "V", ["vue"]),
"svelte": ("#ff3e00", "S", ["svelte"]),
"astro": ("#ff5d01", "A", ["astro"]),
"css": ("#42a5f5", "#", ["css"]),
"sass": ("#ec407a", "#", ["scss", "sass"]),
"less": ("#1565c0", "LS", ["less"]),
"yaml": ("#e53935", "YML", ["yaml", "yml"]),
"toml": ("#8d6e63", "TML", ["toml"]),
"ini": ("#78909c", "INI", ["ini", "cfg", "conf", "properties"]),
"csv": ("#43a047", "CSV", ["csv", "tsv"]),
"graphql": ("#e535ab", "GQ", ["graphql", "gql"]),
"proto": ("#26a69a", "PB", ["proto"]),
"wasm": ("#654ff0", "WA", ["wasm", "wat"]),
"nix": ("#5277c3", "NIX", ["nix"]),
"terraform": ("#7b42bc", "TF", ["tf", "tfvars", "hcl"]),
"powershell": ("#0277bd", "PS", ["ps1", "psm1", "psd1"]),
"batch": ("#546e7a", "BAT", ["bat", "cmd"]),
"tex": ("#008080", "TEX", ["tex", "sty", "bib"]),
"vim": ("#43a047", "VI", ["vim"]),
"diff": ("#66bb6a", "+-", ["diff", "patch"]),
"log": ("#78909c", "LOG", ["log"]),
"notebook": ("#f57c00", "NB", ["ipynb"]),
"http": ("#3f51b5", "API", ["http", "rest"]),
}
GLYPH_EXTENSIONS = {
"text": ["txt", "text"],
"markdown": ["md", "markdown", "mdx"],
"image": ["png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "avif", "tif", "tiff", "icns"],
"svg": ["svg"],
"pdf": ["pdf"],
"archive": ["zip", "tar", "gz", "tgz", "bz2", "xz", "7z", "rar", "zst"],
"lock": ["lock"],
"shell": ["sh", "bash", "zsh", "fish"],
"database": ["sql", "sqlite", "db", "prisma"],
"html": ["html", "htm", "xhtml"],
"xml": ["xml", "xsd", "xsl", "plist", "iml"],
"json": ["json", "jsonc", "json5"],
"react": ["jsx"],
"react-ts": ["tsx"],
"test": [f"{kind}.{ext}" for kind in ("test", "spec") for ext in ("ts", "tsx", "js", "jsx", "mjs", "mts")]
+ ["test.rs", "test.py"],
"font": ["ttf", "otf", "woff", "woff2"],
"audio": ["mp3", "wav", "ogg", "flac", "m4a"],
"video": ["mp4", "mov", "webm", "mkv", "avi"],
"env": ["env"],
}
FILE_NAMES = {
"readme": ["readme", "readme.md", "readme.txt", "readme.rst"],
"license": ["license", "licence", "license.md", "licence.md", "license.txt", "copying", "notice"],
"git": [".gitignore", ".gitattributes", ".gitmodules", ".gitkeep", ".mailmap"],
"docker": ["dockerfile", "containerfile", ".dockerignore", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"],
"npm": ["package.json", ".npmrc", ".npmignore"],
"cargo": ["cargo.toml", "rust-toolchain", "rust-toolchain.toml"],
"gomod": ["go.mod", "go.sum", "go.work"],
"pnpm": ["pnpm-workspace.yaml"],
"lock": ["cargo.lock", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb", "poetry.lock", "uv.lock", "gemfile.lock", "composer.lock"],
"env": [".env", ".env.local", ".env.development", ".env.production", ".env.example"],
"tsconfig": ["tsconfig.json", "jsconfig.json", "tsconfig.base.json"],
"vite": ["vite.config.ts", "vite.config.js", "vite.config.mts", "vitest.config.ts", "vitest.config.mts"],
"makefile": ["makefile", "gnumakefile", "justfile", "cmakelists.txt"],
"editorconfig": [".editorconfig", ".prettierrc", ".prettierignore", ".oxlintrc.json", ".eslintrc", ".eslintrc.json", "mise.toml"],
"settings": [".clippy.toml", "clippy.toml", "rustfmt.toml", ".rustfmt.toml", "deny.toml"],
}
# folder name → (icon id, colour)
FOLDERS = {
"folder-src": ("#4caf50", ["src", "source", "sources", "lib", "libs", "pkg", "internal", "crates", "packages"]),
"folder-test": ("#8bc34a", ["test", "tests", "__tests__", "spec", "specs", "e2e", "fixtures", "testdata"]),
"folder-docs": ("#42a5f5", ["doc", "docs", "documentation", "wiki"]),
"folder-dist": ("#e57373", ["dist", "build", "out", "target", "bin", "obj", "release", "coverage"]),
"folder-deps": ("#9ccc65", ["node_modules", "vendor", ".venv", "venv", "third_party", "deps"]),
"folder-git": ("#ff7043", [".git", ".github", ".gitlab", ".husky"]),
"folder-config": ("#78909c", ["config", "configs", ".config", ".vscode", ".idea", ".cargo", "settings", "etc"]),
"folder-assets": ("#26a69a", ["assets", "static", "public", "images", "img", "icons", "fonts", "media", "resources", "res"]),
"folder-scripts": ("#ffb74d", ["scripts", "script", "tools", "tasks", "ci"]),
"folder-components": ("#ffca28", ["components", "ui", "widgets", "views", "pages", "features"]),
"folder-styles": ("#ec407a", ["styles", "css", "scss", "theme", "themes"]),
"folder-plugins": ("#ab47bc", ["plugins", "extensions", "addons", "sdk"]),
"folder-i18n": ("#5c6bc0", ["i18n", "locales", "locale", "lang", "translations"]),
"folder-api": ("#26c6da", ["api", "server", "routes", "handlers", "rpc"]),
"folder-db": ("#ffa726", ["db", "database", "migrations", "sql", "schema"]),
}
def main() -> None:
OUT.mkdir(exist_ok=True)
for old in OUT.glob("*.svg"):
old.unlink()
icons: dict[str, str] = {}
def add(name: str, content: str) -> None:
assert name not in icons, f"duplicate icon {name}"
icons[name] = content
(OUT / f"{name}.svg").write_text(content)
for name, content in GLYPHS.items():
add(name, content)
add("folder", folder("#90a4ae"))
add("folder-open", folder("#90a4ae", open_=True))
extensions: dict[str, str] = {}
def map_ext(ext: str, icon: str) -> None:
assert ext not in extensions, f"extension {ext} mapped twice"
extensions[ext] = icon
for name, (color, label, exts, *fg) in LANGUAGES.items():
add(name, badge(color, label, *fg))
for ext in exts:
map_ext(ext, name)
for icon, exts in GLYPH_EXTENSIONS.items():
for ext in exts:
map_ext(ext, icon)
file_names = {n: icon for icon, names in FILE_NAMES.items() for n in names}
folder_names: dict[str, str] = {}
for icon, (color, names) in FOLDERS.items():
add(icon, folder(color))
for n in names:
assert n not in folder_names, f"folder {n} mapped twice"
folder_names[n] = icon
for mapping in (extensions, file_names, folder_names):
for key, icon in mapping.items():
assert icon in icons, f"{key} → unknown icon {icon}"
theme = {
"iconDefinitions": {name: f"icons/{name}.svg" for name in sorted(icons)},
"file": "file",
"folder": "folder",
"folderExpanded": "folder-open",
"fileExtensions": dict(sorted(extensions.items())),
"fileNames": dict(sorted(file_names.items())),
"folderNames": dict(sorted(folder_names.items())),
}
(OUT / "theme.json").write_text(json.dumps(theme, indent=2) + "\n")
print(f"{len(icons)} icons, {len(extensions)} extensions, {len(file_names)} file names, {len(folder_names)} folders")
if __name__ == "__main__":
main()