blob: 0cf914681e189e23206d0eb1b0ab7e36529d7d74 [file]
/**
* How a project's absolute path is shown next to its name.
*
* The path is machine data and stays mono, but on the projects list it was
* outweighing the thing it belongs to: a full absolute path wrapped to two
* lines under a one-line project name, so the row read as a path with a title
* rather than a project with a location.
*
* Two rules, in this order:
*
* 1. Collapse the home directory to `~`. It is the single longest constant
* prefix in a personal machine's paths and carries no information — every
* project under it shares it.
* 2. Keep the TAIL when it still does not fit. A path's identity lives at its
* end: `.../experiments/astryx-design-system` says which project this is,
* while `/Users/someone/Developer/exp...` says which machine it is on. Head
* truncation is the opposite of what `text-overflow: ellipsis` does by
* default, which is why it needs saying here rather than in CSS alone.
*
* The full path always remains available as the element's tooltip, so nothing
* is actually hidden — only de-emphasised.
*/
/** Collapse a home-directory prefix to `~`. */
export function collapseHomePath(path: string, homePath: string | undefined): string {
if (!homePath) return path;
// Trailing separators would otherwise leave `~/` for the home dir itself.
const home = homePath.replace(/[/\\]+$/, '');
if (home === '') return path;
if (path === home) return '~';
// Only a real path boundary counts: `/Users/mak` must not match inside
// `/Users/maka-agent`.
if (path.startsWith(`${home}/`) || path.startsWith(`${home}\\`)) {
return `~${path.slice(home.length)}`;
}
return path;
}
/**
* Shorten from the head, keeping the last `maxLength` characters.
*
* Returns the path unchanged when it already fits, so short paths never gain
* a leading ellipsis they did not need.
*/
export function truncatePathHead(path: string, maxLength: number): string {
if (maxLength <= 1 || path.length <= maxLength) return path;
// The ellipsis costs one character of the budget, so the kept tail is
// maxLength - 1 and the result is exactly maxLength wide.
return `…${path.slice(path.length - (maxLength - 1))}`;
}
export interface ProjectPathDisplay {
/** What the row renders. */
text: string;
/** What the tooltip carries — the unabbreviated path, always. */
title: string;
}
export function projectPathDisplay(
path: string,
options: { homePath?: string; maxLength?: number } = {},
): ProjectPathDisplay {
const collapsed = collapseHomePath(path, options.homePath);
return {
text: truncatePathHead(collapsed, options.maxLength ?? 52),
// The ORIGINAL path, not the `~` form: a tooltip exists to answer "where
// exactly is this", and `~` is precisely the part that was elided.
title: path,
};
}