mirror of
https://github.com/jarrodwatts/claude-hud.git
synced 2026-04-16 06:32:39 +00:00
@@ -159,6 +159,8 @@ Chinese HUD labels are available as an explicit opt-in. English stays the defaul
|
||||
| `gitStatus.enabled` | boolean | true | Show git branch in HUD |
|
||||
| `gitStatus.showDirty` | boolean | true | Show `*` for uncommitted changes |
|
||||
| `gitStatus.showAheadBehind` | boolean | false | Show `↑N ↓N` for ahead/behind remote |
|
||||
| `gitStatus.pushWarningThreshold` | number | 0 | Color the ahead count with the warning color at or above this unpushed-commit count (`0` disables it) |
|
||||
| `gitStatus.pushCriticalThreshold` | number | 0 | Color the ahead count with the critical color at or above this unpushed-commit count (`0` disables it) |
|
||||
| `gitStatus.showFileStats` | boolean | false | Show file change counts `!M +A ✘D ?U` |
|
||||
| `display.showModel` | boolean | true | Show model name `[Opus]` |
|
||||
| `display.showContextBar` | boolean | true | Show visual context bar `████░░░░░░` |
|
||||
|
||||
@@ -69,6 +69,8 @@ export interface HudConfig {
|
||||
showDirty: boolean;
|
||||
showAheadBehind: boolean;
|
||||
showFileStats: boolean;
|
||||
pushWarningThreshold: number;
|
||||
pushCriticalThreshold: number;
|
||||
};
|
||||
display: {
|
||||
showModel: boolean;
|
||||
@@ -111,6 +113,8 @@ export const DEFAULT_CONFIG: HudConfig = {
|
||||
showDirty: true,
|
||||
showAheadBehind: false,
|
||||
showFileStats: false,
|
||||
pushWarningThreshold: 0,
|
||||
pushCriticalThreshold: 0,
|
||||
},
|
||||
display: {
|
||||
showModel: true,
|
||||
@@ -263,6 +267,13 @@ function validateThreshold(value: unknown, max = 100): number {
|
||||
return Math.max(0, Math.min(max, value));
|
||||
}
|
||||
|
||||
function validateCountThreshold(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
|
||||
export function mergeConfig(userConfig: Partial<HudConfig>): HudConfig {
|
||||
const migrated = migrateConfig(userConfig);
|
||||
const language = validateLanguage(migrated.language)
|
||||
@@ -296,6 +307,8 @@ export function mergeConfig(userConfig: Partial<HudConfig>): HudConfig {
|
||||
showFileStats: typeof migrated.gitStatus?.showFileStats === 'boolean'
|
||||
? migrated.gitStatus.showFileStats
|
||||
: DEFAULT_CONFIG.gitStatus.showFileStats,
|
||||
pushWarningThreshold: validateCountThreshold(migrated.gitStatus?.pushWarningThreshold),
|
||||
pushCriticalThreshold: validateCountThreshold(migrated.gitStatus?.pushCriticalThreshold),
|
||||
};
|
||||
|
||||
const display = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as path from 'node:path';
|
||||
import type { RenderContext } from '../../types.js';
|
||||
import { getModelName, formatModelName, getProviderLabel } from '../../stdin.js';
|
||||
import { getOutputSpeed } from '../../speed-tracker.js';
|
||||
import { git as gitColor, gitBranch as gitBranchColor, label, model as modelColor, project as projectColor, red, green, yellow, dim, custom as customColor } from '../colors.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';
|
||||
|
||||
function hyperlink(uri: string, text: string): string {
|
||||
@@ -45,7 +45,9 @@ export function renderProjectLine(ctx: RenderContext): string | null {
|
||||
const gitInner: string[] = [linkedBranch];
|
||||
|
||||
if (gitConfig?.showAheadBehind) {
|
||||
if (ctx.gitStatus.ahead > 0) gitInner.push(gitBranchColor(`↑${ctx.gitStatus.ahead}`, colors));
|
||||
if (ctx.gitStatus.ahead > 0) {
|
||||
gitInner.push(formatAheadCount(ctx.gitStatus.ahead, gitConfig, colors));
|
||||
}
|
||||
if (ctx.gitStatus.behind > 0) gitInner.push(gitBranchColor(`↓${ctx.gitStatus.behind}`, colors));
|
||||
}
|
||||
|
||||
@@ -105,6 +107,26 @@ export function renderProjectLine(ctx: RenderContext): string | null {
|
||||
return parts.join(' \u2502 ');
|
||||
}
|
||||
|
||||
function formatAheadCount(
|
||||
ahead: number,
|
||||
gitConfig: RenderContext['config']['gitStatus'] | undefined,
|
||||
colors: RenderContext['config']['colors'] | undefined,
|
||||
): string {
|
||||
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: RenderContext, terminalWidth: number | null = null): string | null {
|
||||
const gitConfig = ctx.config?.gitStatus;
|
||||
if (!(gitConfig?.showFileStats ?? false)) return null;
|
||||
|
||||
@@ -41,6 +41,8 @@ test('loadConfig returns valid config structure', async () => {
|
||||
assert.equal(typeof config.gitStatus.enabled, 'boolean');
|
||||
assert.equal(typeof config.gitStatus.showDirty, 'boolean');
|
||||
assert.equal(typeof config.gitStatus.showAheadBehind, 'boolean');
|
||||
assert.equal(typeof config.gitStatus.pushWarningThreshold, 'number');
|
||||
assert.equal(typeof config.gitStatus.pushCriticalThreshold, 'number');
|
||||
|
||||
// display object with expected properties
|
||||
assert.equal(typeof config.display, 'object');
|
||||
@@ -114,6 +116,20 @@ test('mergeConfig preserves explicit showMemoryUsage=true', () => {
|
||||
assert.equal(config.display.showMemoryUsage, true);
|
||||
});
|
||||
|
||||
test('mergeConfig defaults git push thresholds to disabled', () => {
|
||||
const config = mergeConfig({});
|
||||
assert.equal(config.gitStatus.pushWarningThreshold, 0);
|
||||
assert.equal(config.gitStatus.pushCriticalThreshold, 0);
|
||||
});
|
||||
|
||||
test('mergeConfig preserves explicit git push thresholds', () => {
|
||||
const config = mergeConfig({
|
||||
gitStatus: { pushWarningThreshold: 15, pushCriticalThreshold: 30 },
|
||||
});
|
||||
assert.equal(config.gitStatus.pushWarningThreshold, 15);
|
||||
assert.equal(config.gitStatus.pushCriticalThreshold, 30);
|
||||
});
|
||||
|
||||
test('mergeConfig defaults showOutputStyle to false', () => {
|
||||
const config = mergeConfig({});
|
||||
assert.equal(config.display.showOutputStyle, false);
|
||||
|
||||
@@ -49,7 +49,7 @@ function baseContext() {
|
||||
showSeparators: false,
|
||||
pathLevels: 1,
|
||||
elementOrder: ['project', 'context', 'usage', 'memory', 'environment', 'tools', 'agents', 'todos'],
|
||||
gitStatus: { enabled: true, showDirty: true, showAheadBehind: false, showFileStats: false },
|
||||
gitStatus: { enabled: true, showDirty: true, showAheadBehind: false, showFileStats: false, pushWarningThreshold: 0, pushCriticalThreshold: 0 },
|
||||
display: { showModel: true, showProject: true, showContextBar: true, contextValue: 'percent', showConfigCounts: true, showDuration: true, showSpeed: false, showTokenBreakdown: true, showUsage: true, usageBarEnabled: false, showTools: true, showAgents: true, showTodos: true, showSessionTokens: false, showSessionName: false, showClaudeCodeVersion: false, showMemoryUsage: false, showOutputStyle: false, autocompactBuffer: 'enabled', usageThreshold: 0, sevenDayThreshold: 80, environmentThreshold: 0, customLine: '' },
|
||||
colors: {
|
||||
context: 'green',
|
||||
@@ -1501,6 +1501,8 @@ test('renderSessionLine combines showFileStats with showDirty and showAheadBehin
|
||||
showDirty: true,
|
||||
showAheadBehind: true,
|
||||
showFileStats: true,
|
||||
pushWarningThreshold: 0,
|
||||
pushCriticalThreshold: 0,
|
||||
};
|
||||
ctx.gitStatus = {
|
||||
branch: 'feature',
|
||||
@@ -1518,6 +1520,38 @@ test('renderSessionLine combines showFileStats with showDirty and showAheadBehin
|
||||
assert.ok(line.includes('✘1'), 'expected deleted count');
|
||||
});
|
||||
|
||||
test('renderProjectLine colors ahead count at warning threshold', () => {
|
||||
const ctx = baseContext();
|
||||
ctx.config.gitStatus = {
|
||||
enabled: true,
|
||||
showDirty: true,
|
||||
showAheadBehind: true,
|
||||
showFileStats: false,
|
||||
pushWarningThreshold: 10,
|
||||
pushCriticalThreshold: 20,
|
||||
};
|
||||
ctx.gitStatus = { branch: 'main', isDirty: false, ahead: 12, behind: 0 };
|
||||
|
||||
const line = renderProjectLine(ctx);
|
||||
assert.ok(line?.includes('\x1b[33m↑12\x1b[0m'), 'ahead count should use warning color');
|
||||
});
|
||||
|
||||
test('renderProjectLine colors ahead count at critical threshold', () => {
|
||||
const ctx = baseContext();
|
||||
ctx.config.gitStatus = {
|
||||
enabled: true,
|
||||
showDirty: true,
|
||||
showAheadBehind: true,
|
||||
showFileStats: false,
|
||||
pushWarningThreshold: 10,
|
||||
pushCriticalThreshold: 20,
|
||||
};
|
||||
ctx.gitStatus = { branch: 'main', isDirty: false, ahead: 25, behind: 0 };
|
||||
|
||||
const line = renderProjectLine(ctx);
|
||||
assert.ok(line?.includes('\x1b[31m↑25\x1b[0m'), 'ahead count should use critical color');
|
||||
});
|
||||
|
||||
test('renderGitFilesLine renders tracked files with per-file line diffs', () => {
|
||||
const ctx = baseContext();
|
||||
ctx.stdin.cwd = '/tmp/my-project';
|
||||
|
||||
Reference in New Issue
Block a user