docs/SIDEBAR.md: a dormant session takes the lowest precedence in the sidebar tree, so a rebooted host's live work never sorts under a pile of dormant cards.
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
// 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;
|
|
|
|
/** docs/SIDEBAR.md: a dormant session takes the lowest precedence in the
|
|
* ordering, so live work always sorts above it regardless of name. */
|
|
function dormantRank(node: TreeNode): number {
|
|
return node.kind === "leaf" && node.session.status === "Dormant" ? 1 : 0;
|
|
}
|
|
|
|
export function compareNodes(a: TreeNode, b: TreeNode): number {
|
|
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1;
|
|
const rank = dormantRank(a) - dormantRank(b);
|
|
if (rank !== 0) return rank;
|
|
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): TreeNode {
|
|
const root: TreeFolder = { kind: "folder", name: "", path: "", children: [] };
|
|
|
|
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) : "";
|
|
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;
|
|
}
|