jarrodwatts--claude-hud
e39bafc6d7
Build dist / build (push) Has been cancelled
CI / test (18.x) (push) Has been cancelled
CI / test (20.x) (push) Has been cancelled
224 行
9.7 KiB
JavaScript
224 行
9.7 KiB
JavaScript
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { formatModelName, resolveModelName } from '../../stdin.js';
|
|
import { getOutputSpeed } from '../../speed-tracker.js';
|
|
import { git as gitColor, gitBranch as gitBranchColor, warning as warningColor, critical as criticalColor, label, model as modelColor, project as projectColor, red, green, yellow, dim, custom as customColor } from '../colors.js';
|
|
import { t } from '../../i18n/index.js';
|
|
import { renderCostEstimate } from './cost.js';
|
|
import { renderAdvisorLine } from './advisor.js';
|
|
import { normalizeAddedDirs, sanitize as sanitizeDisplayText, basenameOf, truncateBasename, MAX_RENDERED_ADDED_DIRS } from './added-dirs.js';
|
|
import { getFileHref, safeHyperlink } from '../../utils/hyperlinks.js';
|
|
import { formatModelDisplay } from '../model-display.js';
|
|
import { formatAuthSegment } from '../../auth.js';
|
|
import { formatProjectPath } from '../project-path.js';
|
|
import { DEFAULT_CONFIG, DEFAULT_PROJECT_LINE_ORDER } from '../../config.js';
|
|
import { orderFirstLineParts } from '../first-line-order.js';
|
|
import { getVcsDisplayState } from '../vcs-status.js';
|
|
function resolvePathWithinCwd(cwd, candidatePath) {
|
|
const resolvedCwd = path.resolve(cwd);
|
|
const resolvedPath = path.resolve(cwd, candidatePath);
|
|
const relative = path.relative(resolvedCwd, resolvedPath);
|
|
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
return resolvedPath;
|
|
}
|
|
return null;
|
|
}
|
|
export function renderProjectLine(ctx) {
|
|
const display = ctx.config?.display;
|
|
const colors = ctx.config?.colors;
|
|
const parts = [];
|
|
const push = (text, key = null) => parts.push({ key, text });
|
|
const customLine = display?.customLine;
|
|
const customLinePosition = display?.customLinePosition ?? 'last';
|
|
if (customLine && customLinePosition === 'first') {
|
|
push(customColor(customLine, colors));
|
|
}
|
|
if (display?.showModel !== false) {
|
|
const model = formatModelName(resolveModelName(ctx.stdin, ctx.transcript, ctx.config?.display?.modelSource), ctx.config?.display?.modelFormat, ctx.config?.display?.modelOverride);
|
|
const modelDisplay = formatModelDisplay(model, ctx);
|
|
push(modelColor(`[${modelDisplay}]`, colors), 'model');
|
|
}
|
|
let projectPart = null;
|
|
if (display?.showProject !== false && ctx.stdin.cwd) {
|
|
const pathLevels = ctx.config?.pathLevels ?? 1;
|
|
const projectPath = formatProjectPath(ctx.stdin.cwd, pathLevels);
|
|
const coloredProject = projectColor(projectPath, colors);
|
|
projectPart = safeHyperlink(getFileHref(ctx.stdin.cwd), coloredProject);
|
|
}
|
|
let addedDirsPart = null;
|
|
const addedDirs = normalizeAddedDirs(ctx.stdin.workspace?.added_dirs);
|
|
const addedDirsLayout = display?.addedDirsLayout ?? 'inline';
|
|
if (display?.showAddedDirs !== false && addedDirsLayout === 'inline' && addedDirs.length > 0) {
|
|
const visible = addedDirs.slice(0, MAX_RENDERED_ADDED_DIRS);
|
|
const overflow = addedDirs.length - visible.length;
|
|
const rendered = visible.map((dir) => {
|
|
const name = truncateBasename(sanitizeDisplayText(basenameOf(dir)));
|
|
const text = dim(`+${name}`);
|
|
return safeHyperlink(getFileHref(dir), text);
|
|
});
|
|
if (overflow > 0) {
|
|
rendered.push(dim(`+${overflow} more`));
|
|
}
|
|
addedDirsPart = rendered.join(' ');
|
|
}
|
|
let gitPart = '';
|
|
const vcs = getVcsDisplayState(ctx.gitStatus, ctx.config);
|
|
const gitConfig = ctx.config.gitStatus ?? DEFAULT_CONFIG.gitStatus;
|
|
const branchOverflow = vcs?.branchOverflow ?? gitConfig.branchOverflow;
|
|
if (vcs) {
|
|
const branchText = vcs.branch + (vcs.dirty ? '*' : '');
|
|
const coloredBranch = gitBranchColor(branchText, colors);
|
|
const linkedBranch = safeHyperlink(vcs.branchUrl, coloredBranch);
|
|
const gitInner = [linkedBranch];
|
|
if (vcs.ahead > 0) {
|
|
gitInner.push(formatAheadCount(vcs.ahead, gitConfig, colors));
|
|
}
|
|
if (vcs.behind > 0)
|
|
gitInner.push(gitBranchColor(`↓${vcs.behind}`, colors));
|
|
if (vcs.lineDiff) {
|
|
const { added, deleted } = vcs.lineDiff;
|
|
const diffParts = [];
|
|
if (added > 0)
|
|
diffParts.push(green(`+${added}`));
|
|
if (deleted > 0)
|
|
diffParts.push(red(`-${deleted}`));
|
|
if (diffParts.length > 0) {
|
|
gitInner.push(`[${diffParts.join(' ')}]`);
|
|
}
|
|
}
|
|
if (vcs.conflict) {
|
|
gitInner.push(criticalColor('!conflict', colors));
|
|
}
|
|
const vcsLabel = vcs.kind === 'jj' ? 'jj:(' : 'git:(';
|
|
gitPart = `${gitColor(vcsLabel, colors)}${gitInner.join(' ')}${gitColor(')', colors)}`;
|
|
}
|
|
const projectWithDirs = projectPart && addedDirsPart
|
|
? `${projectPart} ${addedDirsPart}`
|
|
: projectPart ?? addedDirsPart;
|
|
if (projectWithDirs && gitPart) {
|
|
if (branchOverflow === 'wrap') {
|
|
push(projectWithDirs, 'project');
|
|
push(gitPart, 'project');
|
|
}
|
|
else {
|
|
push(`${projectWithDirs} ${gitPart}`, 'project');
|
|
}
|
|
}
|
|
else if (projectWithDirs) {
|
|
push(projectWithDirs, 'project');
|
|
}
|
|
else if (gitPart) {
|
|
push(gitPart, 'project');
|
|
}
|
|
// Advisor model sits inline with the model/project/git badge so the
|
|
// configured /advisor is visible on the first line at a glance.
|
|
if (display?.showAdvisor) {
|
|
const advisorPart = renderAdvisorLine(ctx);
|
|
if (advisorPart) {
|
|
push(advisorPart, 'advisor');
|
|
}
|
|
}
|
|
if (display?.showSessionName && ctx.transcript.sessionName) {
|
|
push(label(ctx.transcript.sessionName, colors), 'sessionName');
|
|
}
|
|
if (display?.showClaudeCodeVersion && ctx.claudeCodeVersion) {
|
|
push(label(`CC v${ctx.claudeCodeVersion}`, colors), 'version');
|
|
}
|
|
if (ctx.extraLabel) {
|
|
push(label(ctx.extraLabel, colors), 'extra');
|
|
}
|
|
if (display?.showDuration === true && ctx.sessionDuration) {
|
|
push(label(`⏱️ ${ctx.sessionDuration}`, colors), 'duration');
|
|
}
|
|
const costEstimate = renderCostEstimate(ctx);
|
|
if (costEstimate) {
|
|
push(costEstimate, 'cost');
|
|
}
|
|
if (display?.showSpeed) {
|
|
const speed = getOutputSpeed(ctx.stdin);
|
|
if (speed !== null) {
|
|
push(label(`${t('format.out')}: ${speed.toFixed(1)} ${t('format.tokPerSec')}`, colors), 'speed');
|
|
}
|
|
}
|
|
const authSegment = formatAuthSegment(ctx.authInfo, display);
|
|
if (authSegment) {
|
|
push(label(authSegment, colors), 'auth');
|
|
}
|
|
if (customLine && customLinePosition === 'last') {
|
|
push(customColor(customLine, colors));
|
|
}
|
|
if (parts.length === 0) {
|
|
return null;
|
|
}
|
|
const order = ctx.config?.projectLineOrder ?? DEFAULT_PROJECT_LINE_ORDER;
|
|
return orderFirstLineParts(parts, order).join(' \u2502 ');
|
|
}
|
|
function formatAheadCount(ahead, gitConfig, colors) {
|
|
const value = `↑${ahead}`;
|
|
const criticalThreshold = gitConfig?.pushCriticalThreshold ?? 0;
|
|
const warningThreshold = gitConfig?.pushWarningThreshold ?? 0;
|
|
if (criticalThreshold > 0 && ahead >= criticalThreshold) {
|
|
return criticalColor(value, colors);
|
|
}
|
|
if (warningThreshold > 0 && ahead >= warningThreshold) {
|
|
return warningColor(value, colors);
|
|
}
|
|
return gitBranchColor(value, colors);
|
|
}
|
|
export function renderGitFilesLine(ctx, terminalWidth = null) {
|
|
const gitConfig = ctx.config?.gitStatus;
|
|
if (!(gitConfig?.showFileStats ?? false))
|
|
return null;
|
|
if (!ctx.gitStatus?.fileStats)
|
|
return null;
|
|
const { trackedFiles, untracked } = ctx.gitStatus.fileStats;
|
|
if (trackedFiles.length === 0 && untracked === 0)
|
|
return null;
|
|
if (terminalWidth !== null && terminalWidth < 60)
|
|
return null;
|
|
const cwd = ctx.stdin.cwd;
|
|
const sorted = [...trackedFiles].sort((a, b) => {
|
|
try {
|
|
const aPath = cwd ? resolvePathWithinCwd(cwd, a.fullPath) : null;
|
|
const bPath = cwd ? resolvePathWithinCwd(cwd, b.fullPath) : null;
|
|
const aMtime = aPath ? fs.statSync(aPath).mtimeMs : 0;
|
|
const bMtime = bPath ? fs.statSync(bPath).mtimeMs : 0;
|
|
return bMtime - aMtime;
|
|
}
|
|
catch {
|
|
return 0;
|
|
}
|
|
});
|
|
const shown = sorted.slice(0, 6);
|
|
const overflow = sorted.length - shown.length;
|
|
const statParts = [];
|
|
for (const trackedFile of shown) {
|
|
const prefix = trackedFile.type === 'added' ? green('+') : trackedFile.type === 'deleted' ? red('-') : yellow('~');
|
|
const safeBasename = sanitizeDisplayText(trackedFile.basename);
|
|
const coloredName = trackedFile.type === 'added'
|
|
? green(safeBasename)
|
|
: trackedFile.type === 'deleted'
|
|
? red(safeBasename)
|
|
: yellow(safeBasename);
|
|
const resolvedPath = cwd ? resolvePathWithinCwd(cwd, trackedFile.fullPath) : null;
|
|
const linkedName = resolvedPath ? safeHyperlink(getFileHref(resolvedPath), coloredName) : coloredName;
|
|
let entry = `${prefix}${linkedName}`;
|
|
if (trackedFile.lineDiff) {
|
|
const diffParts = [];
|
|
if (trackedFile.lineDiff.added > 0)
|
|
diffParts.push(green(`+${trackedFile.lineDiff.added}`));
|
|
if (trackedFile.lineDiff.deleted > 0)
|
|
diffParts.push(red(`-${trackedFile.lineDiff.deleted}`));
|
|
if (diffParts.length > 0) {
|
|
entry += dim(`(${diffParts.join(' ')})`);
|
|
}
|
|
}
|
|
statParts.push(entry);
|
|
}
|
|
if (overflow > 0)
|
|
statParts.push(dim(`+${overflow} more`));
|
|
if (untracked > 0)
|
|
statParts.push(dim(`?${untracked}`));
|
|
return statParts.join(' ');
|
|
}
|
|
//# sourceMappingURL=project.js.map
|