Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/segments/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
formatCacheTimerElapsed,
formatCacheTimerRemaining,
collapseHome,
isPathWithin,
pathBelow,
minutesUntilReset,
pacePercentage,
BLOCK_WINDOW_MINUTES,
Expand Down Expand Up @@ -869,9 +869,13 @@ export class SegmentRenderer {
}

if (projectDir && projectDir !== currentDir) {
if (isPathWithin(currentDir, projectDir)) {
const relativePath = currentDir.slice(projectDir.length + 1);
return relativePath || projectDir.split(/[\\/]/).pop() || "project";
const rest = pathBelow(currentDir, projectDir);
if (rest !== null) {
return (
rest.slice(1) ||
projectDir.split(/[\\/]/).filter(Boolean).pop() ||
"project"
);
}
}

Expand Down
29 changes: 19 additions & 10 deletions src/utils/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,25 +135,34 @@ export function formatBurnRate(rate: number | null | undefined): string {
return rate < 1 ? `${(rate * 100).toFixed(0)}c/h` : `$${rate.toFixed(2)}/h`;
}

function isSeparator(char: string | undefined): boolean {
return char === "/" || char === "\\";
}

/**
* Whether `dirPath` is `baseDir` or inside it. A bare prefix check would also
* match a sibling such as `/home/al` for `/home/alex`.
* The part of `dirPath` below `baseDir`: "" when they are the same directory,
* otherwise a path starting with a separator. Null when `dirPath` is outside
* `baseDir`. Matching whole segments keeps `/home/al` from claiming
* `/home/alex`, and ignoring the base's trailing separators makes `/home/al/`
* behave like `/home/al`. A root base such as `/` keeps its separator, so it
* only matches itself.
*/
export function isPathWithin(dirPath: string, baseDir: string): boolean {
if (!dirPath.startsWith(baseDir)) return false;
const next = dirPath.charAt(baseDir.length);
return next === "" || next === "/" || next === "\\";
export function pathBelow(dirPath: string, baseDir: string): string | null {
let end = baseDir.length;
while (end > 1 && isSeparator(baseDir[end - 1])) end--;
const base = baseDir.slice(0, end);
if (!dirPath.startsWith(base)) return null;
const rest = dirPath.slice(base.length);
return rest === "" || isSeparator(rest[0]) ? rest : null;
}

export function collapseHome(dirPath: string, homeDir?: string): string {
const home =
homeDir ??
globalThis.process?.env?.HOME ??
globalThis.process?.env?.USERPROFILE;
if (home && isPathWithin(dirPath, home)) {
return `~${dirPath.slice(home.length)}`;
}
return dirPath;
const rest = home ? pathBelow(dirPath, home) : null;
return rest === null ? dirPath : `~${rest}`;
}

export function formatTimeRemaining(totalMinutes: number): string {
Expand Down
44 changes: 44 additions & 0 deletions test/formatters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
collapseHome,
formatDuration,
formatModelName,
formatTokens,
pathBelow,
} from "../src/utils/formatters";

describe("formatModelName", () => {
Expand Down Expand Up @@ -187,3 +189,45 @@ describe("formatDuration", () => {
expect(formatDuration(86399)).toBe("1.0d");
});
});

describe("pathBelow", () => {
it("returns the remainder below the base", () => {
expect(pathBelow("/home/al", "/home/al")).toBe("");
expect(pathBelow("/home/al/proj", "/home/al")).toBe("/proj");
expect(pathBelow("C:\\Users\\al\\proj", "C:\\Users\\al")).toBe("\\proj");
});

it("matches whole segments only", () => {
expect(pathBelow("/home/alex/proj", "/home/al")).toBeNull();
expect(pathBelow("/srv/app-web", "/srv/app")).toBeNull();
});

it("ignores trailing separators on the base", () => {
expect(pathBelow("/home/al/proj", "/home/al/")).toBe("/proj");
expect(pathBelow("/home/al", "/home/al/")).toBe("");
expect(pathBelow("C:\\Users\\al\\proj", "C:\\Users\\al\\")).toBe("\\proj");
expect(pathBelow("/home/alex", "/home/al/")).toBeNull();
});

it("treats a root base as matching only itself", () => {
expect(pathBelow("/", "/")).toBe("");
expect(pathBelow("/home/al", "/")).toBeNull();
});
});

describe("collapseHome", () => {
it("collapses paths under a home dir with a trailing separator", () => {
expect(collapseHome("/home/al/proj", "/home/al/")).toBe("~/proj");
expect(collapseHome("/home/al", "/home/al/")).toBe("~");
expect(collapseHome("C:\\Users\\al\\proj", "C:\\Users\\al\\")).toBe(
"~\\proj",
);
});

it("leaves paths outside home unchanged", () => {
expect(collapseHome("/home/alex/proj", "/home/al/")).toBe(
"/home/alex/proj",
);
expect(collapseHome("/home/al/proj", "/")).toBe("/home/al/proj");
});
});
44 changes: 44 additions & 0 deletions test/segments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,50 @@ describe("Segment Time Logic", () => {

expect(result.text).toBe("/srv/app-web");
});

it("should collapse HOME when it ends in a separator", () => {
process.env.HOME = "/home/al/";
const hookData: ClaudeHookData = {
hook_event_name: "Status",
session_id: "test",
transcript_path: "/tmp/test.json",
cwd: "/home/al/proj",
model: { id: "claude-3-5-sonnet", display_name: "Claude" },
workspace: {
current_dir: "/home/al/proj",
project_dir: "/home/al/proj",
},
};

const result = renderer.renderDirectory(hookData, colors, {
enabled: true,
style: "full",
});

expect(result.text).toBe("~/proj");
});

it("should show the path below a project dir that ends in a separator", () => {
process.env.HOME = "/home/user";
const hookData: ClaudeHookData = {
hook_event_name: "Status",
session_id: "test",
transcript_path: "/tmp/test.json",
cwd: "/srv/app/src",
model: { id: "claude-3-5-sonnet", display_name: "Claude" },
workspace: {
current_dir: "/srv/app/src",
project_dir: "/srv/app/",
},
};

const result = renderer.renderDirectory(hookData, colors, {
enabled: true,
style: "full",
});

expect(result.text).toBe("src");
});
});

describe("Version Segment", () => {
Expand Down
Loading