// Directory grouping for the session catalog. Pure: no DOM, no module state, // so it can be tested directly. import type { Session } from "./types.js"; import { abbreviateCwd, sessionName, sessionTreePath } from "./types.js"; export interface TreeFolder { kind: "folder"; name: string; path: string; children: TreeNode[]; } export interface TreeLeaf { kind: "leaf"; session: Session; } export type TreeNode = TreeFolder | TreeLeaf; export function compareNodes(a: TreeNode, b: TreeNode): number { if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1; const an = a.kind === "folder" ? a.name : sessionName(a.session); const bn = b.kind === "folder" ? b.name : sessionName(b.session); return an.localeCompare(bn); } export function buildTree(sessions: Session[], home: string | undefined, filter: string): TreeNode { const root: TreeFolder = { kind: "folder", name: "", path: "", children: [] }; const lc = filter.trim().toLowerCase(); for (const s of sessions) { // Grouped by repository, so a worktree session lands under its repo // rather than in a folder of its own (#99). const cwd = sessionTreePath(s); const dir = cwd ? abbreviateCwd(cwd, home) : ""; if (lc) { const haystack = `${sessionName(s)} ${dir}`.toLowerCase(); if (!haystack.includes(lc)) continue; } const parts = dir ? dir.split("/").filter(Boolean) : []; let cursor = root; for (const part of parts) { const path = cursor.path ? `${cursor.path}/${part}` : part; let next = cursor.children.find( (n): n is TreeFolder => n.kind === "folder" && n.name === part, ); if (!next) { next = { kind: "folder", name: part, path, children: [] }; cursor.children.push(next); } cursor = next; } cursor.children.push({ kind: "leaf", session: s }); } const sortRec = (n: TreeFolder) => { n.children.sort(compareNodes); for (const c of n.children) if (c.kind === "folder") sortRec(c); }; sortRec(root); // The root's sole child (usually `~`) has no sibling to distinguish it // from, so it folds into every one of its own children, not just a lone // one — unlike the chain merge below, and only here at the very top. if (root.children.length === 1 && root.children[0].kind === "folder") { const only = root.children[0]; root.children = only.children.map((c) => c.kind === "folder" ? { ...c, name: `${only.name}/${c.name}` } : c, ); } // Collapse linear directory chains so `~/git.naps.pt/naps62/maestro` reads as // one compact folder rather than four nested ones. root.children = root.children.map((c) => (c.kind === "folder" ? mergeLinear(c) : c)); return root; } /** Fold a folder with a single folder-child into one node (`a` + `b` → `a/b`), * repeating down the chain, then recurse into the surviving branches. The merged * node keeps the deepest path so collapse/expand state stays stable as it grows. */ export function mergeLinear(folder: TreeFolder): TreeFolder { let cur = folder; while (cur.children.length === 1 && cur.children[0].kind === "folder") { const child = cur.children[0]; cur = { kind: "folder", name: `${cur.name}/${child.name}`, path: child.path, children: child.children, }; } cur.children = cur.children.map((c) => c.kind === "folder" ? mergeLinear(c) : c, ); return cur; }