Something went wrong. Try again.
Training wheels for the terminal
Something went wrong. Try again.
4.9 kB · 154 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155import { 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, };}