/** Programming languages by file extension, with GitHub linguist colors. */
const LANGUAGES: Record<string, { name: string; color: string }> = {};
function define(name: string, color: string, exts: string[]) {
for (const ext of exts) LANGUAGES[ext] = { name, color };
}
define("TypeScript", "#3178c6", ["ts", "tsx", "mts", "cts"]);
define("JavaScript", "#f1e05a", ["js", "jsx", "mjs", "cjs"]);
define("CSS", "#663399", ["css"]);
define("SCSS", "#c6538c", ["scss", "sass"]);
define("HTML", "#e34c26", ["html", "htm"]);
define("Vue", "#41b883", ["vue"]);
define("Svelte", "#ff3e00", ["svelte"]);
define("Astro", "#ff5a03", ["astro"]);
define("Python", "#3572a5", ["py"]);
define("Go", "#00add8", ["go"]);
define("Rust", "#dea584", ["rs"]);
define("Swift", "#f05138", ["swift"]);
define("Kotlin", "#a97bff", ["kt", "kts"]);
define("Java", "#b07219", ["java"]);
define("C", "#555555", ["c", "h"]);
define("C++", "#f34b7d", ["cpp", "cc", "cxx", "hpp", "hh"]);
define("C#", "#178600", ["cs"]);
define("Ruby", "#701516", ["rb"]);
define("PHP", "#4f5d95", ["php"]);
define("Shell", "#89e051", ["sh", "bash", "zsh", "fish"]);
define("Lua", "#000080", ["lua"]);
define("Zig", "#ec915c", ["zig"]);
define("Dart", "#00b4ab", ["dart"]);
define("Elixir", "#6e4a7e", ["ex", "exs"]);
define("Nix", "#7e7eff", ["nix"]);
define("Haskell", "#5e5086", ["hs"]);
define("OCaml", "#ef7a08", ["ml", "mli"]);
// Files that aren't code but still deserve a recognizable icon color.
const OTHER_COLORS: Record<string, string> = {
md: "#519aba",
mdx: "#519aba",
json: "#cbcb41",
jsonc: "#cbcb41",
yml: "#cb4b16",
yaml: "#cb4b16",
toml: "#9c4221",
svg: "#ffb13b",
png: "#a074c4",
jpg: "#a074c4",
jpeg: "#a074c4",
gif: "#a074c4",
webp: "#a074c4",
};
const VENDORED = /(^|\/)(node_modules|vendor|dist|build|\.next|third_party)\/|\.min\.(js|css)$/;
function extension(filename: string) {
const base = filename.split("/").pop() ?? "";
if (base === "Dockerfile" || base.endsWith(".dockerfile")) return "dockerfile";
const dot = base.lastIndexOf(".");
return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
}
export function languageFor(filename: string) {
const ext = extension(filename);
if (ext === "dockerfile") return { name: "Dockerfile", color: "#384d54" };
return LANGUAGES[ext] ?? null;
}
/** A color for a file's icon, or null for a neutral one. */
export function fileColor(filename: string) {
return languageFor(filename)?.color ?? OTHER_COLORS[extension(filename)] ?? null;
}
export type LanguageShare = { name: string; color: string; percent: number };
/** Turns (path, size) pairs into a language breakdown, largest first. */
export function languageBreakdown(files: { path: string; size: number }[]): LanguageShare[] {
const bytes = new Map<string, { color: string; size: number }>();
for (const f of files) {
if (VENDORED.test(f.path)) continue;
const lang = languageFor(f.path);
if (!lang) continue;
const entry = bytes.get(lang.name) ?? { color: lang.color, size: 0 };
entry.size += f.size;
bytes.set(lang.name, entry);
}
const total = [...bytes.values()].reduce((sum, e) => sum + e.size, 0);
if (!total) return [];
return [...bytes.entries()]
.map(([name, e]) => ({ name, color: e.color, percent: (e.size / total) * 100 }))
.sort((a, b) => b.percent - a.percent);
}