153 lines
5 KiB
TypeScript
153 lines
5 KiB
TypeScript
/**
|
|
* Path display utilities for shortening file paths in tight UI spaces.
|
|
*
|
|
* Strategy:
|
|
* 1. Strip project root to make relative
|
|
* 2. Replace home directory with ~
|
|
* 3. Middle-truncate if still too long, preserving first and last segments
|
|
*
|
|
* Also provides resolveAbsolutePath() for clipboard copy (~ → real home, relative → absolute).
|
|
*/
|
|
|
|
import { splitPath } from '@shared/utils/platformPath';
|
|
|
|
function isWindowsAbsolutePath(input: string): boolean {
|
|
return /^[A-Za-z]:[/\\]/.test(input) || input.startsWith('\\\\') || input.startsWith('//');
|
|
}
|
|
|
|
function comparePath(input: string, caseInsensitive: boolean): string {
|
|
return caseInsensitive ? input.toLowerCase() : input;
|
|
}
|
|
|
|
function pathSeparatorFor(root: string): '/' | '\\' {
|
|
return root.includes('\\') && !root.includes('/') ? '\\' : '/';
|
|
}
|
|
|
|
function joinDisplayPath(root: string, child: string): string {
|
|
const sep = pathSeparatorFor(root);
|
|
return root.replace(/[/\\]$/, '') + sep + child.replace(/[/\\]/g, sep);
|
|
}
|
|
|
|
/**
|
|
* Shorten a file path for display in compact UI elements.
|
|
* Full path should still be available via tooltip (title attribute).
|
|
*
|
|
* Examples:
|
|
* - `/Users/name/.claude/projects/-Users-name-project/memory/MEMORY.md` → `~/.claude/…/memory/MEMORY.md`
|
|
* - `/Users/name/project/.claude/rules/tailwind.md` (with projectRoot) → `.claude/rules/tailwind.md`
|
|
* - `~/.claude/CLAUDE.md` → `~/.claude/CLAUDE.md` (already short)
|
|
*/
|
|
export function shortenDisplayPath(fullPath: string, projectRoot?: string, maxLength = 40): string {
|
|
let p = fullPath;
|
|
|
|
// 1. Make relative to project root
|
|
if (projectRoot) {
|
|
const root = projectRoot.replace(/[/\\]$/, '');
|
|
const caseInsensitive = isWindowsAbsolutePath(p) || isWindowsAbsolutePath(root);
|
|
const pathForCompare = comparePath(p, caseInsensitive);
|
|
const rootForCompare = comparePath(root, caseInsensitive);
|
|
if (
|
|
pathForCompare.startsWith(rootForCompare + '/') ||
|
|
pathForCompare.startsWith(rootForCompare + '\\')
|
|
) {
|
|
p = p.slice(root.length + 1);
|
|
}
|
|
}
|
|
|
|
// 2. Replace home directory with ~
|
|
p = p
|
|
.replace(/^\/Users\/[^/]+/, '~')
|
|
.replace(/^\/home\/[^/]+/, '~')
|
|
.replace(/^[A-Za-z]:\\Users\\[^\\]+/i, '~');
|
|
|
|
// 3. If short enough, return as-is
|
|
if (p.length <= maxLength) return p;
|
|
|
|
// 4. Middle-truncate: keep first meaningful segments + … + last 2 segments
|
|
const sep = p.includes('\\') ? '\\' : '/';
|
|
const segments = p.split(sep);
|
|
|
|
// Determine where content starts (skip leading empty segment from absolute paths or ~)
|
|
let startIdx = 0;
|
|
if (segments[0] === '' || segments[0] === '~') startIdx = 1;
|
|
|
|
// Need at least 4 content segments to truncate the middle
|
|
if (segments.length - startIdx <= 3) return p;
|
|
|
|
const head = segments.slice(0, startIdx + 1).join(sep);
|
|
const tail = segments.slice(-2).join(sep);
|
|
|
|
return `${head}${sep}\u2026${sep}${tail}`;
|
|
}
|
|
|
|
/**
|
|
* Infer the user's home directory from a known absolute project path.
|
|
* Works for macOS (/Users/x), Linux (/home/x), and Windows (C:\Users\x).
|
|
*/
|
|
function inferHomeDir(projectRoot: string): string | null {
|
|
const match =
|
|
/^(\/Users\/[^/]+)/.exec(projectRoot) ??
|
|
/^(\/home\/[^/]+)/.exec(projectRoot) ??
|
|
/^([A-Za-z]:\\Users\\[^\\]+)/i.exec(projectRoot);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Resolve a possibly-shortened path to its full absolute form for clipboard copy.
|
|
*
|
|
* - `~/...` → `/Users/username/...` (home dir inferred from projectRoot)
|
|
* - `src/foo/bar` → `{projectRoot}/src/foo/bar`
|
|
* - Already absolute → returned as-is
|
|
*/
|
|
/**
|
|
* Truncate a project path to ~/relative/path format.
|
|
* Works for macOS (/Users/...), Linux (/home/...) and Windows (C:\Users\...).
|
|
*/
|
|
export function formatProjectPath(path: string): string {
|
|
const p = path.replace(/\\/g, '/');
|
|
|
|
if (p.startsWith('/Users/') || p.startsWith('/home/')) {
|
|
const parts = splitPath(p);
|
|
if (parts.length >= 2) {
|
|
const rest = parts.slice(2).join('/');
|
|
return rest ? `~/${rest}` : '~';
|
|
}
|
|
}
|
|
|
|
if (isWindowsUserPath(path)) {
|
|
const parts = splitPath(p);
|
|
if (parts.length >= 3) {
|
|
const rest = parts.slice(3).join('/');
|
|
return rest ? `~/${rest}` : '~';
|
|
}
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
function isWindowsUserPath(input: string): boolean {
|
|
if (input.length < 10) return false;
|
|
const drive = input.charCodeAt(0);
|
|
const hasDriveLetter =
|
|
((drive >= 65 && drive <= 90) || (drive >= 97 && drive <= 122)) && input[1] === ':';
|
|
return hasDriveLetter && input.slice(2, 9).toLowerCase() === '\\users\\';
|
|
}
|
|
|
|
export function resolveAbsolutePath(filePath: string, projectRoot?: string): string {
|
|
let p = filePath;
|
|
|
|
// Resolve ~ using home dir inferred from projectRoot
|
|
if ((p.startsWith('~/') || p.startsWith('~\\')) && projectRoot) {
|
|
const homeDir = inferHomeDir(projectRoot);
|
|
if (homeDir) {
|
|
p = joinDisplayPath(homeDir, p.slice(2));
|
|
}
|
|
}
|
|
|
|
// Make relative paths absolute by prepending projectRoot
|
|
if (projectRoot && !p.startsWith('/') && !p.startsWith('~') && !isWindowsAbsolutePath(p)) {
|
|
p = joinDisplayPath(projectRoot, p);
|
|
}
|
|
|
|
return p;
|
|
}
|