mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-10-14 06:17:28 +00:00
1 parent
0c6bf4b02c
commit
6edc1b4277
13 files changed
+434
-35
No files matched your search
@@ -97,7 +97,7 @@ All widgets must implement:
|
||||
|
||||
**Available Widgets:**
|
||||
- Model, Version, OutputStyle - Claude Code metadata display
|
||||
- GitBranch, GitChanges, GitWorktree - Git repository status
|
||||
- GitBranch, GitChanges, GitInsertions, GitDeletions, GitWorktree - Git repository status
|
||||
- TokensInput, TokensOutput, TokensCached, TokensTotal - Token usage metrics
|
||||
- ContextLength, ContextPercentage, ContextPercentageUsable - Context window metrics (uses dynamic model-based context windows: 1M for Sonnet 4.5 with [1m] suffix, 200k for all other models)
|
||||
- BlockTimer, SessionClock, SessionCost - Time and cost tracking
|
||||
|
||||
@@ -46,12 +46,14 @@
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
### v2.1.0 - v2.1.3 - Usage widgets, links, and reliability fixes
|
||||
### v2.1.0 - v2.1.4 - Usage widgets, links, new git insertions / deletions widgets, and reliability fixes
|
||||
|
||||
- **🧩 New Usage widgets (v2.1.0)** - Added **Session Usage**, **Weekly Usage**, **Reset Timer**, and **Context Bar** widgets.
|
||||
- **📊 More accurate counts (v2.1.0)** - Usage/context widgets now use new statusline JSON metrics when available for more accurate token and context counts.
|
||||
- **🪟 Windows empty file bug fix (v2.1.1)** - Fixed a Windows issue that could create an empty `c:\dev\null` file.
|
||||
- **🔗 New Link widget (v2.1.3)** - Added a new **Link** widget with clickable OSC8 rendering, preview parity, and raw mode support.
|
||||
- **➕ New Git Insertions widget (v2.1.4)** - Added a dedicated Git widget that shows only uncommitted insertions (e.g., `+42`).
|
||||
- **➖ New Git Deletions widget (v2.1.4)** - Added a dedicated Git widget that shows only uncommitted deletions (e.g., `-10`).
|
||||
|
||||
### v2.0.26 - v2.0.29 - Performance, git internals, and workflow improvements
|
||||
|
||||
@@ -412,6 +414,8 @@ bun run example
|
||||
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
|
||||
- **Git Branch** - Displays current git branch name
|
||||
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
|
||||
- **Git Insertions** - Shows uncommitted insertions only (e.g., "+42")
|
||||
- **Git Deletions** - Shows uncommitted deletions only (e.g., "-10")
|
||||
- **Git Root Dir** - Shows the git repository root directory name
|
||||
- **Git Worktree** - Shows the name of the current git worktree
|
||||
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import {
|
||||
getGitChangeCounts,
|
||||
isInsideGitWorkTree,
|
||||
resolveGitCwd,
|
||||
runGit
|
||||
@@ -20,6 +21,7 @@ const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
describe('git utils', () => {
|
||||
@@ -134,4 +136,35 @@ describe('git utils', () => {
|
||||
expect(isInsideGitWorkTree({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGitChangeCounts', () => {
|
||||
it('sums staged and unstaged insertions/deletions', () => {
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 2 insertions(+), 1 deletion(-)');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 3 insertions(+), 4 deletions(-)');
|
||||
|
||||
expect(getGitChangeCounts({})).toEqual({
|
||||
insertions: 5,
|
||||
deletions: 5
|
||||
});
|
||||
});
|
||||
|
||||
it('handles singular insertion/deletion forms', () => {
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 1 insertion(+), 1 deletion(-)');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(getGitChangeCounts({})).toEqual({
|
||||
insertions: 1,
|
||||
deletions: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('returns zero counts when git diff commands fail', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('git failed'); });
|
||||
|
||||
expect(getGitChangeCounts({})).toEqual({
|
||||
insertions: 0,
|
||||
deletions: 0
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,8 @@ describe('widget catalog', () => {
|
||||
const model = catalog.find(entry => entry.type === 'model');
|
||||
const separator = catalog.find(entry => entry.type === 'separator');
|
||||
const link = catalog.find(entry => entry.type === 'link');
|
||||
const gitInsertions = catalog.find(entry => entry.type === 'git-insertions');
|
||||
const gitDeletions = catalog.find(entry => entry.type === 'git-deletions');
|
||||
|
||||
expect(model?.displayName).toBe('Model');
|
||||
expect(model?.category).toBe('Core');
|
||||
@@ -35,6 +37,10 @@ describe('widget catalog', () => {
|
||||
expect(separator?.category).toBe('Layout');
|
||||
expect(link?.displayName).toBe('Link');
|
||||
expect(link?.category).toBe('Custom');
|
||||
expect(gitInsertions?.displayName).toBe('Git Insertions');
|
||||
expect(gitInsertions?.category).toBe('Git');
|
||||
expect(gitDeletions?.displayName).toBe('Git Deletions');
|
||||
expect(gitDeletions?.category).toBe('Git');
|
||||
});
|
||||
|
||||
it('hides manual separator when default separator is configured', () => {
|
||||
|
||||
@@ -2,6 +2,11 @@ import { execSync } from 'child_process';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
|
||||
export interface GitChangeCounts {
|
||||
insertions: number;
|
||||
deletions: number;
|
||||
}
|
||||
|
||||
export function resolveGitCwd(context: RenderContext): string | undefined {
|
||||
const candidates = [
|
||||
context.data?.cwd,
|
||||
@@ -35,4 +40,26 @@ export function runGit(command: string, context: RenderContext): string | null {
|
||||
|
||||
export function isInsideGitWorkTree(context: RenderContext): boolean {
|
||||
return runGit('rev-parse --is-inside-work-tree', context) === 'true';
|
||||
}
|
||||
|
||||
function parseDiffShortStat(stat: string): GitChangeCounts {
|
||||
const insertMatch = /(\d+)\s+insertions?/.exec(stat);
|
||||
const deleteMatch = /(\d+)\s+deletions?/.exec(stat);
|
||||
|
||||
return {
|
||||
insertions: insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0,
|
||||
deletions: deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0
|
||||
};
|
||||
}
|
||||
|
||||
export function getGitChangeCounts(context: RenderContext): GitChangeCounts {
|
||||
const unstagedStat = runGit('diff --shortstat', context) ?? '';
|
||||
const stagedStat = runGit('diff --cached --shortstat', context) ?? '';
|
||||
const unstagedCounts = parseDiffShortStat(unstagedStat);
|
||||
const stagedCounts = parseDiffShortStat(stagedStat);
|
||||
|
||||
return {
|
||||
insertions: unstagedCounts.insertions + stagedCounts.insertions,
|
||||
deletions: unstagedCounts.deletions + stagedCounts.deletions
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,8 @@ const widgetRegistry = new Map<WidgetItemType, Widget>([
|
||||
['output-style', new widgets.OutputStyleWidget()],
|
||||
['git-branch', new widgets.GitBranchWidget()],
|
||||
['git-changes', new widgets.GitChangesWidget()],
|
||||
['git-insertions', new widgets.GitInsertionsWidget()],
|
||||
['git-deletions', new widgets.GitDeletionsWidget()],
|
||||
['git-root-dir', new widgets.GitRootDirWidget()],
|
||||
['git-worktree', new widgets.GitWorktreeWidget()],
|
||||
['current-working-dir', new widgets.CurrentWorkingDirWidget()],
|
||||
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
getGitChangeCounts,
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitChangesWidget implements Widget {
|
||||
@@ -44,7 +44,7 @@ export class GitChangesWidget implements Widget {
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
|
||||
if (context.isPreview) {
|
||||
@@ -55,35 +55,8 @@ export class GitChangesWidget implements Widget {
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
const changes = this.getGitChanges(context);
|
||||
if (changes)
|
||||
return `(+${changes.insertions},-${changes.deletions})`;
|
||||
else
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
private getGitChanges(context: RenderContext): { insertions: number; deletions: number } | null {
|
||||
let totalInsertions = 0;
|
||||
let totalDeletions = 0;
|
||||
|
||||
const unstagedStat = runGit('diff --shortstat', context) ?? '';
|
||||
const stagedStat = runGit('diff --cached --shortstat', context) ?? '';
|
||||
|
||||
if (unstagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(unstagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(unstagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
if (stagedStat) {
|
||||
const insertMatch = /(\d+) insertion/.exec(stagedStat);
|
||||
const deleteMatch = /(\d+) deletion/.exec(stagedStat);
|
||||
totalInsertions += insertMatch?.[1] ? parseInt(insertMatch[1], 10) : 0;
|
||||
totalDeletions += deleteMatch?.[1] ? parseInt(deleteMatch[1], 10) : 0;
|
||||
}
|
||||
|
||||
return { insertions: totalInsertions, deletions: totalDeletions };
|
||||
const changes = getGitChangeCounts(context);
|
||||
return `(+${changes.insertions},-${changes.deletions})`;
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
getGitChangeCounts,
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitDeletionsWidget implements Widget {
|
||||
getDefaultColor(): string { return 'red'; }
|
||||
getDescription(): string { return 'Shows git deletions count'; }
|
||||
getDisplayName(): string { return 'Git Deletions'; }
|
||||
getCategory(): string { return 'Git'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (hideNoGit) {
|
||||
modifiers.push('hide \'no git\'');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-nogit') {
|
||||
const currentState = item.metadata?.hideNoGit === 'true';
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
hideNoGit: (!currentState).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
|
||||
if (context.isPreview) {
|
||||
return '-10';
|
||||
}
|
||||
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
const changes = getGitChangeCounts(context);
|
||||
return `-${changes.deletions}`;
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import {
|
||||
getGitChangeCounts,
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
export class GitInsertionsWidget implements Widget {
|
||||
getDefaultColor(): string { return 'green'; }
|
||||
getDescription(): string { return 'Shows git insertions count'; }
|
||||
getDisplayName(): string { return 'Git Insertions'; }
|
||||
getCategory(): string { return 'Git'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (hideNoGit) {
|
||||
modifiers.push('hide \'no git\'');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-nogit') {
|
||||
const currentState = item.metadata?.hideNoGit === 'true';
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
hideNoGit: (!currentState).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
|
||||
if (context.isPreview) {
|
||||
return '+42';
|
||||
}
|
||||
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
return hideNoGit ? null : '(no git)';
|
||||
}
|
||||
|
||||
const changes = getGitChangeCounts(context);
|
||||
return `+${changes.insertions}`;
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import { GitDeletionsWidget } from '../GitDeletions';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitDeletionsWidget();
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-deletions',
|
||||
type: 'git-deletions',
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('GitDeletionsWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render preview', () => {
|
||||
expect(render({ isPreview: true })).toBe('-10');
|
||||
});
|
||||
|
||||
it('should render combined staged and unstaged deletions', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 2 insertions(+), 1 deletion(-)');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 3 insertions(+), 4 deletions(-)');
|
||||
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('-5');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[2]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render zero count when repo is clean', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(render()).toBe('-0');
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
|
||||
it('should hide no git when configured', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should render no git when command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
|
||||
it('should disable raw value support', () => {
|
||||
const widget = new GitDeletionsWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import { GitInsertionsWidget } from '../GitInsertions';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
|
||||
const mockExecSync = execSync as unknown as {
|
||||
mock: { calls: unknown[][] };
|
||||
mockImplementation: (impl: () => never) => void;
|
||||
mockReturnValue: (value: string) => void;
|
||||
mockReturnValueOnce: (value: string) => void;
|
||||
};
|
||||
|
||||
function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitInsertionsWidget();
|
||||
const context: RenderContext = {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-insertions',
|
||||
type: 'git-insertions',
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('GitInsertionsWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render preview', () => {
|
||||
expect(render({ isPreview: true })).toBe('+42');
|
||||
});
|
||||
|
||||
it('should render combined staged and unstaged insertions', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 2 insertions(+), 1 deletion(-)');
|
||||
mockExecSync.mockReturnValueOnce('1 file changed, 3 insertions(+), 4 deletions(-)');
|
||||
|
||||
expect(render({ cwd: '/tmp/worktree' })).toBe('+5');
|
||||
expect(mockExecSync.mock.calls[0]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[1]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
expect(mockExecSync.mock.calls[2]?.[1]).toEqual({
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore'],
|
||||
cwd: '/tmp/worktree'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render zero count when repo is clean', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
mockExecSync.mockReturnValueOnce('');
|
||||
|
||||
expect(render()).toBe('+0');
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
|
||||
it('should hide no git when configured', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should render no git when command fails', () => {
|
||||
mockExecSync.mockImplementation(() => { throw new Error('No git'); });
|
||||
|
||||
expect(render()).toBe('(no git)');
|
||||
});
|
||||
|
||||
it('should disable raw value support', () => {
|
||||
const widget = new GitInsertionsWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ export { ModelWidget } from './Model';
|
||||
export { OutputStyleWidget } from './OutputStyle';
|
||||
export { GitBranchWidget } from './GitBranch';
|
||||
export { GitChangesWidget } from './GitChanges';
|
||||
export { GitInsertionsWidget } from './GitInsertions';
|
||||
export { GitDeletionsWidget } from './GitDeletions';
|
||||
export { GitRootDirWidget } from './GitRootDir';
|
||||
export { GitWorktreeWidget } from './GitWorktree';
|
||||
export { TokensInputWidget } from './TokensInput';
|
||||
|
||||
Reference in new issue
Block a user