import { useMemo } from "react"; import { useTerminalContext } from "./useTerminalContext"; import { useAppSelector } from "../../app/hooks"; // These helpers deliberately avoid a global "is this Windows?" flag. The paths we // handle come from the shell's cwd and native file dialogs, so we infer the // convention (separator, drive/UNC root, case-sensitivity) from the shape of the // path itself. That keeps the functions pure and lets a single build serve both // POSIX and Windows. /** Splits a path into its non-empty segments, accepting either separator. */ function splitPathIntoSegments(path: string): string[] { return path.split(/[\\/]+/).filter((segment) => segment.length > 0); } /** Prefer backslashes if any of the given paths already use them (Windows). */ function preferredSeparator(...paths: string[]): "\\" | "/" { return paths.some((p) => p.includes("\\")) ? "\\" : "/"; } interface ParsedPath { /** "" for POSIX, "C:" for a Windows drive, "\\\\server\\share" for a UNC path. */ root: string; segments: string[]; } /** Splits a path into its root (drive letter / UNC share / POSIX "") and segments. */ function parsePath(path: string): ParsedPath { // UNC path: \\server\share\rest or //server/share/rest const unc = path.match(/^[\\/]{2}([^\\/]+)[\\/]([^\\/]+)(.*)$/); if (unc) { const [, server, share, rest] = unc; return { root: `\\\\${server}\\${share}`, segments: splitPathIntoSegments(rest) }; } // Windows drive path: C:\rest or C:/rest const drive = path.match(/^([A-Za-z]:)[\\/]?(.*)$/); if (drive) { const [, letter, rest] = drive; return { root: letter, segments: splitPathIntoSegments(rest) }; } // POSIX absolute (leading /) or any relative path. return { root: "", segments: splitPathIntoSegments(path) }; } /** True for POSIX (`/…`), Windows drive (`C:\…`, `C:/…`), and UNC (`\\…`) paths. */ export function isAbsolutePath(path: string): boolean { return ( path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(path) ); } export function toRelativePath( targetAbsolutePath: string, fromAbsoluteCwd: string, ): string { const target = parsePath(targetAbsolutePath); const from = parsePath(fromAbsoluteCwd); const sep = preferredSeparator(targetAbsolutePath, fromAbsoluteCwd); // Different roots — e.g. C: vs D:, or two UNC shares — have no common ancestor, // so no relative path exists. Fall back to the absolute path unchanged. if (target.root.toLowerCase() !== from.root.toLowerCase()) { return targetAbsolutePath; } // Windows paths (backslash separator or a drive/UNC root) compare // case-insensitively; POSIX paths are case-sensitive. const caseInsensitive = sep === "\\" || target.root !== "" || from.root !== ""; const equalSegment = (a: string, b: string) => caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b; // Find common prefix length let common = 0; const minLen = Math.min(target.segments.length, from.segments.length); while (common < minLen && equalSegment(target.segments[common], from.segments[common])) { common++; } const ups = from.segments.length - common; const downs = target.segments.slice(common).join(sep); if (ups > 0) { const upPath = Array(ups).fill("..").join(sep); return downs ? `${upPath}${sep}${downs}` : upPath; } if (downs) { return `.${sep}${downs}`; } return "."; } export function toAbsolutePath( relativePath: string, fromAbsoluteCwd: string, ): string { if (isAbsolutePath(relativePath)) { return relativePath; } const sep = preferredSeparator(fromAbsoluteCwd, relativePath); const { root, segments } = parsePath(fromAbsoluteCwd); const stack = [...segments]; for (const segment of splitPathIntoSegments(relativePath)) { if (segment === ".") continue; if (segment === "..") { stack.pop(); continue; } stack.push(segment); } // For a drive/UNC root, the root already carries no trailing separator // ("C:", "\\server\share"); for POSIX the root is "" and the leading // separator does the same job. return `${root}${sep}${stack.join(sep)}`; } export function useDisplayPath( absolutePath: string, _onChange: (path: string) => void, ) { const { cwd } = useTerminalContext(); const useRelative = useAppSelector( (state) => state.commandBuilder.useRelativePaths, ); const path = useMemo(() => { if (!absolutePath || !isAbsolutePath(absolutePath)) { return absolutePath; } if (useRelative) { return toRelativePath(absolutePath, cwd); } return absolutePath; }, [absolutePath, cwd, useRelative]); const onChange = (value: string) => { if (useRelative) { _onChange(toAbsolutePath(value, cwd)); } else { _onChange(value); } }; return { path, onChange, }; }