Vectrum
Sign In
91 lines · 3.3 KB · TypeScript
/** 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);
}