/** * Utility functions for filtering and sorting projects * These are extracted to avoid circular dependencies between state and scanner */ import { PROJECT_MARKERS, type Project, type SortField, type SortDirection } from "../types/index.ts"; /** * Get status priority for sorting (lower = more attention needed) */ function getStatusPriority(project: Project): number { if (project.type === "non-git") return 100; if (!project.status) return 99; let priority = 0; // Dirty repos need attention if (project.status.isDirty) priority -= 40; // Out of sync repos need attention if (project.status.isAhead) priority -= 20; if (project.status.isBehind) priority -= 30; // No remote = might need setup if (!project.status.hasRemote) priority -= 10; return priority; } function getProjectLanguage(project: Project): string { if (!project.projectMarker) return ""; return PROJECT_MARKERS[project.projectMarker] ?? project.projectMarker; } /** * Sort projects based on configuration */ export function sortProjects( projects: Project[], sortBy: SortField, direction: SortDirection ): Project[] { const sorted = [...projects].sort((a, b) => { let comparison = 0; switch (sortBy) { case "name": comparison = a.name.localeCompare(b.name); break; case "branch": { const aBranch = a.status?.currentBranch ?? ""; const bBranch = b.status?.currentBranch ?? ""; comparison = aBranch.localeCompare(bBranch); break; } case "status": // Lower priority = needs more attention // For "desc": most attention-needed first (lowest priority numbers first) // For "asc": least attention-needed first (highest priority numbers first) comparison = getStatusPriority(a) - getStatusPriority(b); // Don't invert for desc - we want lower priority (more important) first return direction === "desc" ? comparison : -comparison; case "sync": { const aStatus = a.status; const bStatus = b.status; const aDelta = aStatus ? (aStatus.unpushedCommits ?? 0) + (aStatus.unpulledCommits ?? 0) : 0; const bDelta = bStatus ? (bStatus.unpushedCommits ?? 0) + (bStatus.unpulledCommits ?? 0) : 0; comparison = aDelta - bDelta; break; } case "language": { const aLang = getProjectLanguage(a).toLowerCase(); const bLang = getProjectLanguage(b).toLowerCase(); comparison = aLang.localeCompare(bLang); break; } case "lastActivity": // Handle both Date objects and string timestamps const aTime = a.status?.lastLocalCommit ? (typeof a.status.lastLocalCommit === 'string' ? new Date(a.status.lastLocalCommit).getTime() : a.status.lastLocalCommit.getTime()) : 0; const bTime = b.status?.lastLocalCommit ? (typeof b.status.lastLocalCommit === 'string' ? new Date(b.status.lastLocalCommit).getTime() : b.status.lastLocalCommit.getTime()) : 0; // For desc: most recent first (higher date value first) // For asc: oldest first (lower date value first) comparison = aTime - bTime; break; case "stars": case "forks": case "size": // Projects don't include GitHub metadata directly; keep stable comparison = 0; break; } return direction === "desc" ? -comparison : comparison; }); return sorted; } /** * Filter projects based on text search */ export function filterProjects(projects: Project[], filterText: string): Project[] { if (!filterText.trim()) return projects; const lower = filterText.toLowerCase(); return projects.filter((p) => { // Match name if (p.name.toLowerCase().includes(lower)) return true; // Match path if (p.path.toLowerCase().includes(lower)) return true; // Match project marker if (p.projectMarker?.toLowerCase().includes(lower)) return true; // Match type if (p.type.toLowerCase().includes(lower)) return true; return false; }); }