export const PROJECT_ICON_NAMES = [ "ai", "book", "cloud", "database", "desktop", "game", "image", "mobile", "music", "package", "security", "server", "shopping", "terminal", "test", "video", "web", "code", "braces", "circuit", "folder-code", "layers", ] as const; export type ProjectIconName = (typeof PROJECT_ICON_NAMES)[number]; export type ProjectIconSelection = | { readonly kind: "lucide"; readonly icon: ProjectIconName } | { readonly kind: "emoji"; readonly icon: ProjectIconName; readonly emoji: string }; interface ProjectIconClass { readonly icon: ProjectIconName; readonly terms: ReadonlyArray; } const PROJECT_ICON_MODEL: ReadonlyArray = [ { icon: "ai", terms: ["ai", "agent", "bot", "gpt", "llm", "ml", "model", "neural"] }, { icon: "mobile", terms: ["android", "expo", "ios", "mobile", "native", "reactnative", "swift"], }, { icon: "desktop", terms: ["desktop", "electron", "linux", "mac", "macos", "tauri", "windows"], }, { icon: "book", terms: ["book", "docs", "documentation", "guide", "handbook", "manual", "wiki"], }, { icon: "security", terms: ["auth", "identity", "oauth", "security", "sso", "vault"], }, { icon: "database", terms: [ "analytics", "data", "database", "db", "mongo", "mysql", "postgres", "redis", "sql", "storage", ], }, { icon: "cloud", terms: [ "aws", "azure", "cloud", "deploy", "devops", "docker", "gcp", "infra", "kubernetes", "terraform", ], }, { icon: "server", terms: ["api", "backend", "gateway", "server", "service", "worker"], }, { icon: "terminal", terms: ["automation", "bash", "cli", "command", "script", "shell", "terminal"], }, { icon: "package", terms: ["component", "kit", "lib", "library", "package", "plugin", "sdk", "toolkit"], }, { icon: "test", terms: ["benchmark", "e2e", "fixture", "spec", "test", "testing"], }, { icon: "shopping", terms: ["cart", "commerce", "market", "shop", "store"], }, { icon: "game", terms: ["game", "gaming", "play"] }, { icon: "music", terms: ["audio", "music", "podcast", "radio", "sound"] }, { icon: "video", terms: ["film", "movie", "stream", "video"] }, { icon: "image", terms: ["camera", "gallery", "image", "photo", "picture"] }, { icon: "web", terms: [ "browser", "frontend", "nextjs", "react", "site", "svelte", "ui", "vue", "web", "website", ], }, ]; const GENERIC_PROJECT_ICONS: ReadonlyArray = [ "code", "braces", "circuit", "folder-code", "layers", ]; const projectIconCache = new Map(); function projectNameTokens(value: string): ReadonlyArray { return value .replace(/([a-z\d])([A-Z])/g, "$1 $2") .toLowerCase() .split(/[^a-z\d]+/) .filter(Boolean); } function termScore(token: string, term: string): number { if (token === term) return 3; if (term.length >= 4 && (token.startsWith(term) || token.endsWith(term))) return 1; return 0; } function stableIndex(value: string, length: number): number { let hash = 2_166_136_261; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16_777_619); } return (hash >>> 0) % length; } export function selectProjectIcon( projectName: string, workspaceRoot: string, ): ProjectIconSelection { const name = projectName.trim() || workspaceRoot.split(/[\\/]+/).findLast(Boolean) || "project"; const cacheKey = name.toLowerCase(); const cachedIcon = projectIconCache.get(cacheKey); if (cachedIcon) return cachedIcon; const tokens = projectNameTokens(name); let bestIcon: ProjectIconName | null = null; let bestScore = 0; for (const projectClass of PROJECT_ICON_MODEL) { let score = 0; for (const token of tokens) { let tokenScore = 0; for (const term of projectClass.terms) { tokenScore = Math.max(tokenScore, termScore(token, term)); } score += tokenScore; } if (score > bestScore) { bestIcon = projectClass.icon; bestScore = score; } } const iconName = bestIcon ?? GENERIC_PROJECT_ICONS[stableIndex(cacheKey, GENERIC_PROJECT_ICONS.length)]!; const icon: ProjectIconSelection = { kind: "lucide", icon: iconName }; projectIconCache.set(cacheKey, icon); return icon; }