mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-05-30 04:20:57 +00:00
refactor: implement phase 3 widget pattern extraction with tests
This commit is contained in:
+27
-78
@@ -12,19 +12,16 @@ import {
|
||||
resolveUsageWindowWithFallback
|
||||
} from '../utils/usage';
|
||||
|
||||
type DisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
function getDisplayMode(item: WidgetItem): DisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
return mode;
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
function isInverted(item: WidgetItem): boolean {
|
||||
return item.metadata?.invert === 'true';
|
||||
}
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
function makeTimerProgressBar(percent: number, width: number): string {
|
||||
const clampedPercent = Math.max(0, Math.min(100, percent));
|
||||
@@ -40,111 +37,63 @@ export class BlockTimerWidget implements Widget {
|
||||
getCategory(): string { return 'Usage'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const mode = getDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
const currentMode = getDisplayMode(item);
|
||||
let nextMode: DisplayMode;
|
||||
|
||||
if (currentMode === 'time') {
|
||||
nextMode = 'progress';
|
||||
} else if (currentMode === 'progress') {
|
||||
nextMode = 'progress-short';
|
||||
} else {
|
||||
nextMode = 'time';
|
||||
}
|
||||
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
return cycleUsageDisplayMode(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
invert: (!isInverted(item)).toString()
|
||||
}
|
||||
};
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getDisplayMode(item);
|
||||
const inverted = isInverted(item);
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = inverted ? 26.1 : 73.9;
|
||||
const prefix = item.rawValue ? '' : 'Block ';
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const barWidth = getUsageProgressBarWidth(displayMode);
|
||||
const progressBar = makeTimerProgressBar(previewPercent, barWidth);
|
||||
return `${prefix}[${progressBar}] ${previewPercent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return item.rawValue ? '3hr 45m' : 'Block: 3hr 45m';
|
||||
return formatRawOrLabeledValue(item, 'Block: ', '3hr 45m');
|
||||
}
|
||||
|
||||
const usageData = fetchUsageData();
|
||||
const window = resolveUsageWindowWithFallback(usageData, context.blockMetrics);
|
||||
|
||||
if (!window) {
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const barWidth = getUsageProgressBarWidth(displayMode);
|
||||
const emptyBar = '░'.repeat(barWidth);
|
||||
return item.rawValue ? `[${emptyBar}] 0.0%` : `Block [${emptyBar}] 0.0%`;
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${emptyBar}] 0.0%`);
|
||||
}
|
||||
|
||||
return item.rawValue ? '0hr 0m' : 'Block: 0hr 0m';
|
||||
return formatRawOrLabeledValue(item, 'Block: ', '0hr 0m');
|
||||
}
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const barWidth = getUsageProgressBarWidth(displayMode);
|
||||
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
|
||||
const progressBar = makeTimerProgressBar(percent, barWidth);
|
||||
const percentage = percent.toFixed(1);
|
||||
|
||||
if (item.rawValue) {
|
||||
return `[${progressBar}] ${percentage}%`;
|
||||
}
|
||||
|
||||
return `Block [${progressBar}] ${percentage}%`;
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${percentage}%`);
|
||||
}
|
||||
|
||||
const elapsedTime = formatUsageDuration(window.elapsedMs);
|
||||
return item.rawValue ? elapsedTime : `Block: ${elapsedTime}`;
|
||||
return formatRawOrLabeledValue(item, 'Block: ', elapsedTime);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
@@ -9,51 +9,41 @@ import type {
|
||||
import { getContextWindowMetrics } from '../utils/context-window';
|
||||
import { getContextConfig } from '../utils/model-context';
|
||||
|
||||
import {
|
||||
getContextInverseModifierText,
|
||||
handleContextInverseAction,
|
||||
isContextInverse
|
||||
} from './shared/context-inverse';
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class ContextPercentageWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows percentage of context window used or remaining'; }
|
||||
getDisplayName(): string { return 'Context %'; }
|
||||
getCategory(): string { return 'Context'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const isInverse = item.metadata?.inverse === 'true';
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (isInverse) {
|
||||
modifiers.push('remaining');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getContextInverseModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-inverse') {
|
||||
const currentState = item.metadata?.inverse === 'true';
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
inverse: (!currentState).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return handleContextInverseAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const isInverse = item.metadata?.inverse === 'true';
|
||||
const isInverse = isContextInverse(item);
|
||||
const contextWindowMetrics = getContextWindowMetrics(context.data);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewValue = isInverse ? '90.7%' : '9.3%';
|
||||
return item.rawValue ? previewValue : `Ctx: ${previewValue}`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx: ', previewValue);
|
||||
}
|
||||
|
||||
if (contextWindowMetrics.usedPercentage !== null) {
|
||||
const displayPercentage = isInverse ? (100 - contextWindowMetrics.usedPercentage) : contextWindowMetrics.usedPercentage;
|
||||
return item.rawValue ? `${displayPercentage.toFixed(1)}%` : `Ctx: ${displayPercentage.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx: ', `${displayPercentage.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
@@ -62,7 +52,7 @@ export class ContextPercentageWidget implements Widget {
|
||||
const contextConfig = getContextConfig(modelId, contextWindowMetrics.windowSize);
|
||||
const usedPercentage = Math.min(100, (context.tokenMetrics.contextLength / contextConfig.maxTokens) * 100);
|
||||
const displayPercentage = isInverse ? (100 - usedPercentage) : usedPercentage;
|
||||
return item.rawValue ? `${displayPercentage.toFixed(1)}%` : `Ctx: ${displayPercentage.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx: ', `${displayPercentage.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,41 +9,31 @@ import type {
|
||||
import { getContextWindowMetrics } from '../utils/context-window';
|
||||
import { getContextConfig } from '../utils/model-context';
|
||||
|
||||
import {
|
||||
getContextInverseModifierText,
|
||||
handleContextInverseAction,
|
||||
isContextInverse
|
||||
} from './shared/context-inverse';
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class ContextPercentageUsableWidget implements Widget {
|
||||
getDefaultColor(): string { return 'green'; }
|
||||
getDescription(): string { return 'Shows percentage of usable context window used or remaining (80% of max before auto-compact)'; }
|
||||
getDisplayName(): string { return 'Context % (usable)'; }
|
||||
getCategory(): string { return 'Context'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const isInverse = item.metadata?.inverse === 'true';
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (isInverse) {
|
||||
modifiers.push('remaining');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getContextInverseModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-inverse') {
|
||||
const currentState = item.metadata?.inverse === 'true';
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
inverse: (!currentState).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
return handleContextInverseAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const isInverse = item.metadata?.inverse === 'true';
|
||||
const isInverse = isContextInverse(item);
|
||||
const model = context.data?.model;
|
||||
const modelId = typeof model === 'string' ? model : model?.id;
|
||||
const contextWindowMetrics = getContextWindowMetrics(context.data);
|
||||
@@ -51,19 +41,19 @@ export class ContextPercentageUsableWidget implements Widget {
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewValue = isInverse ? '88.4%' : '11.6%';
|
||||
return item.rawValue ? previewValue : `Ctx(u): ${previewValue}`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx(u): ', previewValue);
|
||||
}
|
||||
|
||||
if (contextWindowMetrics.contextLengthTokens !== null) {
|
||||
const usedPercentage = Math.min(100, (contextWindowMetrics.contextLengthTokens / contextConfig.usableTokens) * 100);
|
||||
const displayPercentage = isInverse ? (100 - usedPercentage) : usedPercentage;
|
||||
return item.rawValue ? `${displayPercentage.toFixed(1)}%` : `Ctx(u): ${displayPercentage.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx(u): ', `${displayPercentage.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
const usedPercentage = Math.min(100, (context.tokenMetrics.contextLength / contextConfig.usableTokens) * 100);
|
||||
const displayPercentage = isInverse ? (100 - usedPercentage) : usedPercentage;
|
||||
return item.rawValue ? `${displayPercentage.toFixed(1)}%` : `Ctx(u): ${displayPercentage.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Ctx(u): ', `${displayPercentage.toFixed(1)}%`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+11
-23
@@ -11,41 +11,31 @@ import {
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
|
||||
export class GitBranchWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
getDescription(): string { return 'Shows the current git branch name'; }
|
||||
getDisplayName(): string { return 'Git Branch'; }
|
||||
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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? 'main' : '⎇ main';
|
||||
@@ -67,9 +57,7 @@ export class GitBranchWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
+11
-23
@@ -11,41 +11,31 @@ import {
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
|
||||
export class GitChangesWidget implements Widget {
|
||||
getDefaultColor(): string { return 'yellow'; }
|
||||
getDescription(): string { return 'Shows git changes count (+insertions, -deletions)'; }
|
||||
getDisplayName(): string { return 'Git Changes'; }
|
||||
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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return '(+42,-10)';
|
||||
@@ -60,9 +50,7 @@ export class GitChangesWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
|
||||
+11
-23
@@ -11,41 +11,31 @@ import {
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return '-10';
|
||||
@@ -60,9 +50,7 @@ export class GitDeletionsWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
|
||||
@@ -11,41 +11,31 @@ import {
|
||||
isInsideGitWorkTree
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return '+42';
|
||||
@@ -60,9 +50,7 @@ export class GitInsertionsWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
|
||||
+11
-23
@@ -11,41 +11,31 @@ import {
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
|
||||
export class GitRootDirWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows the git repository root directory name'; }
|
||||
getDisplayName(): string { return 'Git Root Dir'; }
|
||||
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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return 'my-repo';
|
||||
@@ -76,9 +66,7 @@ export class GitRootDirWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
|
||||
+11
-23
@@ -10,41 +10,31 @@ import {
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
|
||||
export class GitWorktreeWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows the current git worktree name'; }
|
||||
getDisplayName(): string { return 'Git Worktree'; }
|
||||
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
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext): string | null {
|
||||
const hideNoGit = item.metadata?.hideNoGit === 'true';
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
|
||||
if (context.isPreview)
|
||||
return item.rawValue ? 'main' : '𖠰 main';
|
||||
@@ -85,9 +75,7 @@ export class GitWorktreeWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
];
|
||||
return getHideNoGitKeybinds();
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
+23
-74
@@ -13,19 +13,16 @@ import {
|
||||
resolveUsageWindowWithFallback
|
||||
} from '../utils/usage';
|
||||
|
||||
type DisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
function getDisplayMode(item: WidgetItem): DisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
return mode;
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
function isInverted(item: WidgetItem): boolean {
|
||||
return item.metadata?.invert === 'true';
|
||||
}
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
function makeTimerProgressBar(percent: number, width: number): string {
|
||||
const clampedPercent = Math.max(0, Math.min(100, percent));
|
||||
@@ -41,81 +38,38 @@ export class ResetTimerWidget implements Widget {
|
||||
getCategory(): string { return 'Usage'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const mode = getDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
const currentMode = getDisplayMode(item);
|
||||
let nextMode: DisplayMode;
|
||||
|
||||
if (currentMode === 'time') {
|
||||
nextMode = 'progress';
|
||||
} else if (currentMode === 'progress') {
|
||||
nextMode = 'progress-short';
|
||||
} else {
|
||||
nextMode = 'time';
|
||||
}
|
||||
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
return cycleUsageDisplayMode(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
invert: (!isInverted(item)).toString()
|
||||
}
|
||||
};
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getDisplayMode(item);
|
||||
const inverted = isInverted(item);
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = inverted ? 90.0 : 10.0;
|
||||
const prefix = item.rawValue ? '' : 'Reset ';
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const barWidth = getUsageProgressBarWidth(displayMode);
|
||||
const progressBar = makeTimerProgressBar(previewPercent, barWidth);
|
||||
return `${prefix}[${progressBar}] ${previewPercent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return item.rawValue ? '4hr 30m' : 'Reset: 4hr 30m';
|
||||
return formatRawOrLabeledValue(item, 'Reset: ', '4hr 30m');
|
||||
}
|
||||
|
||||
const usageData = fetchUsageData();
|
||||
@@ -129,21 +83,16 @@ export class ResetTimerWidget implements Widget {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const barWidth = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const barWidth = getUsageProgressBarWidth(displayMode);
|
||||
const percent = inverted ? window.remainingPercent : window.elapsedPercent;
|
||||
const progressBar = makeTimerProgressBar(percent, barWidth);
|
||||
const percentage = percent.toFixed(1);
|
||||
|
||||
if (item.rawValue) {
|
||||
return `[${progressBar}] ${percentage}%`;
|
||||
}
|
||||
|
||||
return `Reset [${progressBar}] ${percentage}%`;
|
||||
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${percentage}%`);
|
||||
}
|
||||
|
||||
const remainingTime = formatUsageDuration(window.remainingMs);
|
||||
return item.rawValue ? remainingTime : `Reset: ${remainingTime}`;
|
||||
return formatRawOrLabeledValue(item, 'Reset: ', remainingTime);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
+23
-68
@@ -12,19 +12,16 @@ import {
|
||||
makeUsageProgressBar
|
||||
} from '../utils/usage';
|
||||
|
||||
type DisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
function getDisplayMode(item: WidgetItem): DisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
return mode;
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
function isInverted(item: WidgetItem): boolean {
|
||||
return item.metadata?.invert === 'true';
|
||||
}
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
export class SessionUsageWidget implements Widget {
|
||||
getDefaultColor(): string { return 'brightBlue'; }
|
||||
@@ -33,81 +30,39 @@ export class SessionUsageWidget implements Widget {
|
||||
getCategory(): string { return 'Usage'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const mode = getDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
const currentMode = getDisplayMode(item);
|
||||
let nextMode: DisplayMode;
|
||||
|
||||
if (currentMode === 'time') {
|
||||
nextMode = 'progress';
|
||||
} else if (currentMode === 'progress') {
|
||||
nextMode = 'progress-short';
|
||||
} else {
|
||||
nextMode = 'time';
|
||||
}
|
||||
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
return cycleUsageDisplayMode(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
invert: (!isInverted(item)).toString()
|
||||
}
|
||||
};
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getDisplayMode(item);
|
||||
const inverted = isInverted(item);
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = 20;
|
||||
const renderedPercent = inverted ? 100 - previewPercent : previewPercent;
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const width = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const width = getUsageProgressBarWidth(displayMode);
|
||||
const progressDisplay = `${makeUsageProgressBar(renderedPercent, width)} ${renderedPercent.toFixed(1)}%`;
|
||||
return item.rawValue ? progressDisplay : `Session: ${progressDisplay}`;
|
||||
return formatRawOrLabeledValue(item, 'Session: ', progressDisplay);
|
||||
}
|
||||
|
||||
return item.rawValue ? `${previewPercent.toFixed(1)}%` : `Session: ${previewPercent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Session: ', `${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
const data = fetchUsageData();
|
||||
@@ -117,14 +72,14 @@ export class SessionUsageWidget implements Widget {
|
||||
return null;
|
||||
|
||||
const percent = Math.max(0, Math.min(100, data.sessionUsage));
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const width = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const width = getUsageProgressBarWidth(displayMode);
|
||||
const renderedPercent = inverted ? 100 - percent : percent;
|
||||
const progressDisplay = `${makeUsageProgressBar(renderedPercent, width)} ${renderedPercent.toFixed(1)}%`;
|
||||
return item.rawValue ? progressDisplay : `Session: ${progressDisplay}`;
|
||||
return formatRawOrLabeledValue(item, 'Session: ', progressDisplay);
|
||||
}
|
||||
|
||||
return item.rawValue ? `${percent.toFixed(1)}%` : `Session: ${percent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Session: ', `${percent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
} from '../types/Widget';
|
||||
import { formatTokens } from '../utils/renderer';
|
||||
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class TokensCachedWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows cached token count for the current session'; }
|
||||
@@ -18,11 +20,11 @@ export class TokensCachedWidget implements Widget {
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? '12k' : 'Cached: 12k';
|
||||
return formatRawOrLabeledValue(item, 'Cached: ', '12k');
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
return item.rawValue ? formatTokens(context.tokenMetrics.cachedTokens) : `Cached: ${formatTokens(context.tokenMetrics.cachedTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'Cached: ', formatTokens(context.tokenMetrics.cachedTokens));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
import { getContextWindowInputTotalTokens } from '../utils/context-window';
|
||||
import { formatTokens } from '../utils/renderer';
|
||||
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class TokensInputWidget implements Widget {
|
||||
getDefaultColor(): string { return 'blue'; }
|
||||
getDescription(): string { return 'Shows input token count for the current session'; }
|
||||
@@ -19,16 +21,16 @@ export class TokensInputWidget implements Widget {
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? '15.2k' : 'In: 15.2k';
|
||||
return formatRawOrLabeledValue(item, 'In: ', '15.2k');
|
||||
}
|
||||
|
||||
const inputTotalTokens = getContextWindowInputTotalTokens(context.data);
|
||||
if (inputTotalTokens !== null) {
|
||||
return item.rawValue ? formatTokens(inputTotalTokens) : `In: ${formatTokens(inputTotalTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'In: ', formatTokens(inputTotalTokens));
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
return item.rawValue ? formatTokens(context.tokenMetrics.inputTokens) : `In: ${formatTokens(context.tokenMetrics.inputTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'In: ', formatTokens(context.tokenMetrics.inputTokens));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
import { getContextWindowOutputTotalTokens } from '../utils/context-window';
|
||||
import { formatTokens } from '../utils/renderer';
|
||||
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class TokensOutputWidget implements Widget {
|
||||
getDefaultColor(): string { return 'white'; }
|
||||
getDescription(): string { return 'Shows output token count for the current session'; }
|
||||
@@ -19,16 +21,16 @@ export class TokensOutputWidget implements Widget {
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? '3.4k' : 'Out: 3.4k';
|
||||
return formatRawOrLabeledValue(item, 'Out: ', '3.4k');
|
||||
}
|
||||
|
||||
const outputTotalTokens = getContextWindowOutputTotalTokens(context.data);
|
||||
if (outputTotalTokens !== null) {
|
||||
return item.rawValue ? formatTokens(outputTotalTokens) : `Out: ${formatTokens(outputTotalTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'Out: ', formatTokens(outputTotalTokens));
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
return item.rawValue ? formatTokens(context.tokenMetrics.outputTokens) : `Out: ${formatTokens(context.tokenMetrics.outputTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'Out: ', formatTokens(context.tokenMetrics.outputTokens));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import type {
|
||||
} from '../types/Widget';
|
||||
import { formatTokens } from '../utils/renderer';
|
||||
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
|
||||
export class TokensTotalWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
getDescription(): string { return 'Shows total token count (input + output + cache) for the current session'; }
|
||||
@@ -18,11 +20,11 @@ export class TokensTotalWidget implements Widget {
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? '30.6k' : 'Total: 30.6k';
|
||||
return formatRawOrLabeledValue(item, 'Total: ', '30.6k');
|
||||
}
|
||||
|
||||
if (context.tokenMetrics) {
|
||||
return item.rawValue ? formatTokens(context.tokenMetrics.totalTokens) : `Total: ${formatTokens(context.tokenMetrics.totalTokens)}`;
|
||||
return formatRawOrLabeledValue(item, 'Total: ', formatTokens(context.tokenMetrics.totalTokens));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+23
-68
@@ -12,19 +12,16 @@ import {
|
||||
makeUsageProgressBar
|
||||
} from '../utils/usage';
|
||||
|
||||
type DisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
function getDisplayMode(item: WidgetItem): DisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
return mode;
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
function isInverted(item: WidgetItem): boolean {
|
||||
return item.metadata?.invert === 'true';
|
||||
}
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
export class WeeklyUsageWidget implements Widget {
|
||||
getDefaultColor(): string { return 'brightBlue'; }
|
||||
@@ -33,81 +30,39 @@ export class WeeklyUsageWidget implements Widget {
|
||||
getCategory(): string { return 'Usage'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const mode = getDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
const currentMode = getDisplayMode(item);
|
||||
let nextMode: DisplayMode;
|
||||
|
||||
if (currentMode === 'time') {
|
||||
nextMode = 'progress';
|
||||
} else if (currentMode === 'progress') {
|
||||
nextMode = 'progress-short';
|
||||
} else {
|
||||
nextMode = 'time';
|
||||
}
|
||||
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
return cycleUsageDisplayMode(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
invert: (!isInverted(item)).toString()
|
||||
}
|
||||
};
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getDisplayMode(item);
|
||||
const inverted = isInverted(item);
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = 12;
|
||||
const renderedPercent = inverted ? 100 - previewPercent : previewPercent;
|
||||
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const width = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const width = getUsageProgressBarWidth(displayMode);
|
||||
const progressDisplay = `${makeUsageProgressBar(renderedPercent, width)} ${renderedPercent.toFixed(1)}%`;
|
||||
return item.rawValue ? progressDisplay : `Weekly: ${progressDisplay}`;
|
||||
return formatRawOrLabeledValue(item, 'Weekly: ', progressDisplay);
|
||||
}
|
||||
|
||||
return item.rawValue ? `${previewPercent.toFixed(1)}%` : `Weekly: ${previewPercent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Weekly: ', `${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
const data = fetchUsageData();
|
||||
@@ -117,14 +72,14 @@ export class WeeklyUsageWidget implements Widget {
|
||||
return null;
|
||||
|
||||
const percent = Math.max(0, Math.min(100, data.weeklyUsage));
|
||||
if (displayMode === 'progress' || displayMode === 'progress-short') {
|
||||
const width = displayMode === 'progress' ? 32 : 16;
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
const width = getUsageProgressBarWidth(displayMode);
|
||||
const renderedPercent = inverted ? 100 - percent : percent;
|
||||
const progressDisplay = `${makeUsageProgressBar(renderedPercent, width)} ${renderedPercent.toFixed(1)}%`;
|
||||
return item.rawValue ? progressDisplay : `Weekly: ${progressDisplay}`;
|
||||
return formatRawOrLabeledValue(item, 'Weekly: ', progressDisplay);
|
||||
}
|
||||
|
||||
return item.rawValue ? `${percent.toFixed(1)}%` : `Weekly: ${percent.toFixed(1)}%`;
|
||||
return formatRawOrLabeledValue(item, 'Weekly: ', `${percent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
|
||||
@@ -129,4 +129,33 @@ describe('BlockTimerWidget', () => {
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const base: WidgetItem = { id: 'block', type: 'block-timer' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const base: WidgetItem = { id: 'block', type: 'block-timer' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
}).modifierText).toBe('(progress bar, inverted)');
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,25 @@ function render(modelId: string | undefined, contextLength: number, rawValue = f
|
||||
}
|
||||
|
||||
describe('ContextPercentageWidget', () => {
|
||||
it('toggles inverse metadata and editor modifier', () => {
|
||||
const widget = new ContextPercentageWidget();
|
||||
const base: WidgetItem = {
|
||||
id: 'context-percentage',
|
||||
type: 'context-percentage'
|
||||
};
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-inverse', base);
|
||||
const cleared = widget.handleEditorAction('toggle-inverse', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.inverse).toBe('true');
|
||||
expect(cleared?.metadata?.inverse).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { inverse: 'true' }
|
||||
}).modifierText).toBe('(remaining)');
|
||||
});
|
||||
|
||||
it('prefers context_window percentage over token metrics when both exist', () => {
|
||||
const widget = new ContextPercentageWidget();
|
||||
const item: WidgetItem = {
|
||||
|
||||
@@ -34,6 +34,25 @@ function render(modelId: string | undefined, contextLength: number, rawValue = f
|
||||
}
|
||||
|
||||
describe('ContextPercentageUsableWidget', () => {
|
||||
it('toggles inverse metadata and editor modifier', () => {
|
||||
const widget = new ContextPercentageUsableWidget();
|
||||
const base: WidgetItem = {
|
||||
id: 'context-percentage-usable',
|
||||
type: 'context-percentage-usable'
|
||||
};
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-inverse', base);
|
||||
const cleared = widget.handleEditorAction('toggle-inverse', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.inverse).toBe('true');
|
||||
expect(cleared?.metadata?.inverse).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { inverse: 'true' }
|
||||
}).modifierText).toBe('(remaining)');
|
||||
});
|
||||
|
||||
it('prefers context_window usage over token metrics when both exist', () => {
|
||||
const widget = new ContextPercentageUsableWidget();
|
||||
const item: WidgetItem = {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetItem
|
||||
} from '../../types';
|
||||
import { GitBranchWidget } from '../GitBranch';
|
||||
import { GitChangesWidget } from '../GitChanges';
|
||||
import { GitDeletionsWidget } from '../GitDeletions';
|
||||
import { GitInsertionsWidget } from '../GitInsertions';
|
||||
import { GitRootDirWidget } from '../GitRootDir';
|
||||
import { GitWorktreeWidget } from '../GitWorktree';
|
||||
|
||||
type GitWidget = Widget & {
|
||||
getCustomKeybinds: () => CustomKeybind[];
|
||||
handleEditorAction: (action: string, item: WidgetItem) => WidgetItem | null;
|
||||
};
|
||||
|
||||
const cases: { name: string; itemType: string; widget: GitWidget }[] = [
|
||||
{ name: 'GitBranchWidget', itemType: 'git-branch', widget: new GitBranchWidget() },
|
||||
{ name: 'GitChangesWidget', itemType: 'git-changes', widget: new GitChangesWidget() },
|
||||
{ name: 'GitInsertionsWidget', itemType: 'git-insertions', widget: new GitInsertionsWidget() },
|
||||
{ name: 'GitDeletionsWidget', itemType: 'git-deletions', widget: new GitDeletionsWidget() },
|
||||
{ name: 'GitRootDirWidget', itemType: 'git-root-dir', widget: new GitRootDirWidget() },
|
||||
{ name: 'GitWorktreeWidget', itemType: 'git-worktree', widget: new GitWorktreeWidget() }
|
||||
];
|
||||
|
||||
describe('Git widget shared behavior', () => {
|
||||
it.each(cases)('$name should expose hide-no-git keybind', ({ widget }) => {
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(cases)('$name should toggle hideNoGit metadata', ({ widget, itemType }) => {
|
||||
const base: WidgetItem = { id: itemType, type: itemType };
|
||||
const toggledOn = widget.handleEditorAction('toggle-nogit', base);
|
||||
const toggledOff = widget.handleEditorAction('toggle-nogit', toggledOn ?? base);
|
||||
|
||||
expect(toggledOn?.metadata?.hideNoGit).toBe('true');
|
||||
expect(toggledOff?.metadata?.hideNoGit).toBe('false');
|
||||
});
|
||||
|
||||
it.each(cases)('$name should show hide-no-git modifier in editor display', ({ widget, itemType }) => {
|
||||
const display = widget.getEditorDisplay({
|
||||
id: itemType,
|
||||
type: itemType,
|
||||
metadata: { hideNoGit: 'true' }
|
||||
});
|
||||
|
||||
expect(display.modifierText).toBe('(hide \'no git\')');
|
||||
});
|
||||
});
|
||||
@@ -142,4 +142,33 @@ describe('ResetTimerWidget', () => {
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new ResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'reset', type: 'reset-timer' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new ResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'reset', type: 'reset-timer' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
}).modifierText).toBe('(short bar, inverted)');
|
||||
});
|
||||
});
|
||||
@@ -112,4 +112,33 @@ describe('SessionUsageWidget', () => {
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const base: WidgetItem = { id: 'session', type: 'session-usage' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const base: WidgetItem = { id: 'session', type: 'session-usage' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
}).modifierText).toBe('(short bar, inverted)');
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,34 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../types';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import { TokensCachedWidget } from '../TokensCached';
|
||||
import { TokensInputWidget } from '../TokensInput';
|
||||
import { TokensOutputWidget } from '../TokensOutput';
|
||||
import { TokensTotalWidget } from '../TokensTotal';
|
||||
|
||||
vi.mock('../../utils/renderer', () => ({ formatTokens: vi.fn((value: number) => `fmt:${value}`) }));
|
||||
|
||||
async function loadWidgets() {
|
||||
const [{ TokensInputWidget }, { TokensOutputWidget }, { TokensCachedWidget }, { TokensTotalWidget }] = await Promise.all([
|
||||
import('../TokensInput'),
|
||||
import('../TokensOutput'),
|
||||
import('../TokensCached'),
|
||||
import('../TokensTotal')
|
||||
]);
|
||||
|
||||
return {
|
||||
TokensCachedWidget,
|
||||
TokensInputWidget,
|
||||
TokensOutputWidget,
|
||||
TokensTotalWidget
|
||||
};
|
||||
}
|
||||
|
||||
describe('Token widgets', () => {
|
||||
it('use context_window values for input/output and tokenMetrics totals for cached/total', () => {
|
||||
it('use context_window values for input/output and tokenMetrics totals for cached/total', async () => {
|
||||
const { TokensCachedWidget, TokensInputWidget, TokensOutputWidget, TokensTotalWidget } = await loadWidgets();
|
||||
const context: RenderContext = {
|
||||
data: {
|
||||
context_window: {
|
||||
@@ -35,13 +51,14 @@ describe('Token widgets', () => {
|
||||
}
|
||||
};
|
||||
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input' }, context, DEFAULT_SETTINGS)).toBe('In: 1.1k');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output' }, context, DEFAULT_SETTINGS)).toBe('Out: 2.2k');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached' }, context, DEFAULT_SETTINGS)).toBe('Cached: 10.0k');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total' }, context, DEFAULT_SETTINGS)).toBe('Total: 10.0k');
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input' }, context, DEFAULT_SETTINGS)).toBe('In: fmt:1111');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output' }, context, DEFAULT_SETTINGS)).toBe('Out: fmt:2222');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached' }, context, DEFAULT_SETTINGS)).toBe('Cached: fmt:9999');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total' }, context, DEFAULT_SETTINGS)).toBe('Total: fmt:9999');
|
||||
});
|
||||
|
||||
it('fall back to token metrics when context_window data is missing', () => {
|
||||
it('fall back to token metrics when context_window data is missing', async () => {
|
||||
const { TokensCachedWidget, TokensInputWidget, TokensOutputWidget, TokensTotalWidget } = await loadWidgets();
|
||||
const context: RenderContext = {
|
||||
tokenMetrics: {
|
||||
inputTokens: 1200,
|
||||
@@ -52,9 +69,53 @@ describe('Token widgets', () => {
|
||||
}
|
||||
};
|
||||
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input' }, context, DEFAULT_SETTINGS)).toBe('In: 1.2k');
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input' }, context, DEFAULT_SETTINGS)).toBe('In: fmt:1200');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output' }, context, DEFAULT_SETTINGS)).toBe('Out: fmt:3400');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached' }, context, DEFAULT_SETTINGS)).toBe('Cached: fmt:560');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total' }, context, DEFAULT_SETTINGS)).toBe('Total: fmt:5160');
|
||||
});
|
||||
|
||||
it('renders raw values without labels for all token widgets', async () => {
|
||||
const { TokensCachedWidget, TokensInputWidget, TokensOutputWidget, TokensTotalWidget } = await loadWidgets();
|
||||
const context: RenderContext = {
|
||||
data: {
|
||||
context_window: {
|
||||
total_input_tokens: 1111,
|
||||
total_output_tokens: 2222,
|
||||
current_usage: {
|
||||
input_tokens: 300,
|
||||
output_tokens: 400,
|
||||
cache_creation_input_tokens: 50,
|
||||
cache_read_input_tokens: 25
|
||||
}
|
||||
}
|
||||
},
|
||||
tokenMetrics: {
|
||||
inputTokens: 1200,
|
||||
outputTokens: 3400,
|
||||
cachedTokens: 560,
|
||||
totalTokens: 5160,
|
||||
contextLength: 20000
|
||||
}
|
||||
};
|
||||
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('fmt:1111');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('fmt:2222');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('fmt:560');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('fmt:5160');
|
||||
});
|
||||
|
||||
it('renders expected preview labels and raw values for all token widgets', async () => {
|
||||
const { TokensCachedWidget, TokensInputWidget, TokensOutputWidget, TokensTotalWidget } = await loadWidgets();
|
||||
const context: RenderContext = { isPreview: true };
|
||||
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input' }, context, DEFAULT_SETTINGS)).toBe('In: 15.2k');
|
||||
expect(new TokensInputWidget().render({ id: 'in', type: 'tokens-input', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('15.2k');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output' }, context, DEFAULT_SETTINGS)).toBe('Out: 3.4k');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached' }, context, DEFAULT_SETTINGS)).toBe('Cached: 560');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total' }, context, DEFAULT_SETTINGS)).toBe('Total: 5.2k');
|
||||
expect(new TokensOutputWidget().render({ id: 'out', type: 'tokens-output', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('3.4k');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached' }, context, DEFAULT_SETTINGS)).toBe('Cached: 12k');
|
||||
expect(new TokensCachedWidget().render({ id: 'cached', type: 'tokens-cached', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('12k');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total' }, context, DEFAULT_SETTINGS)).toBe('Total: 30.6k');
|
||||
expect(new TokensTotalWidget().render({ id: 'total', type: 'tokens-total', rawValue: true }, context, DEFAULT_SETTINGS)).toBe('30.6k');
|
||||
});
|
||||
});
|
||||
@@ -112,4 +112,33 @@ describe('WeeklyUsageWidget', () => {
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const base: WidgetItem = { id: 'weekly', type: 'weekly-usage' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const base: WidgetItem = { id: 'weekly', type: 'weekly-usage' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
}).modifierText).toBe('(progress bar, inverted)');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
|
||||
import { makeModifierText } from './editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
toggleMetadataFlag
|
||||
} from './metadata';
|
||||
|
||||
const INVERSE_KEY = 'inverse';
|
||||
const TOGGLE_INVERSE_ACTION = 'toggle-inverse';
|
||||
|
||||
export function isContextInverse(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, INVERSE_KEY);
|
||||
}
|
||||
|
||||
export function getContextInverseModifierText(item: WidgetItem): string | undefined {
|
||||
return makeModifierText(isContextInverse(item) ? ['remaining'] : []);
|
||||
}
|
||||
|
||||
export function handleContextInverseAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action !== TOGGLE_INVERSE_ACTION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toggleMetadataFlag(item, INVERSE_KEY);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function makeModifierText(modifiers: string[]): string | undefined {
|
||||
return modifiers.length > 0 ? `(${modifiers.join(', ')})` : undefined;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type {
|
||||
CustomKeybind,
|
||||
WidgetItem
|
||||
} from '../../types/Widget';
|
||||
|
||||
import { makeModifierText } from './editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
toggleMetadataFlag
|
||||
} from './metadata';
|
||||
|
||||
const HIDE_NO_GIT_KEY = 'hideNoGit';
|
||||
const TOGGLE_NO_GIT_ACTION = 'toggle-nogit';
|
||||
|
||||
const HIDE_NO_GIT_KEYBIND: CustomKeybind = {
|
||||
key: 'h',
|
||||
label: '(h)ide \'no git\' message',
|
||||
action: TOGGLE_NO_GIT_ACTION
|
||||
};
|
||||
|
||||
export function isHideNoGitEnabled(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, HIDE_NO_GIT_KEY);
|
||||
}
|
||||
|
||||
export function getHideNoGitModifierText(item: WidgetItem): string | undefined {
|
||||
return makeModifierText(isHideNoGitEnabled(item) ? ['hide \'no git\''] : []);
|
||||
}
|
||||
|
||||
export function handleToggleNoGitAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action !== TOGGLE_NO_GIT_ACTION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toggleMetadataFlag(item, HIDE_NO_GIT_KEY);
|
||||
}
|
||||
|
||||
export function getHideNoGitKeybinds(): CustomKeybind[] {
|
||||
return [HIDE_NO_GIT_KEYBIND];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
|
||||
export function isMetadataFlagEnabled(item: WidgetItem, key: string): boolean {
|
||||
return item.metadata?.[key] === 'true';
|
||||
}
|
||||
|
||||
export function toggleMetadataFlag(item: WidgetItem, key: string): WidgetItem {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
[key]: (!isMetadataFlagEnabled(item, key)).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
|
||||
export function formatRawOrLabeledValue(item: WidgetItem, labelPrefix: string, value: string): string {
|
||||
return item.rawValue ? value : `${labelPrefix}${value}`;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
|
||||
import { makeModifierText } from './editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
toggleMetadataFlag
|
||||
} from './metadata';
|
||||
|
||||
export type UsageDisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
export function getUsageDisplayMode(item: WidgetItem): UsageDisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
return mode;
|
||||
}
|
||||
return 'time';
|
||||
}
|
||||
|
||||
export function isUsageProgressMode(mode: UsageDisplayMode): boolean {
|
||||
return mode === 'progress' || mode === 'progress-short';
|
||||
}
|
||||
|
||||
export function getUsageProgressBarWidth(mode: UsageDisplayMode): number {
|
||||
return mode === 'progress' ? 32 : 16;
|
||||
}
|
||||
|
||||
export function isUsageInverted(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, 'invert');
|
||||
}
|
||||
|
||||
export function getUsageDisplayModifierText(item: WidgetItem): string | undefined {
|
||||
const mode = getUsageDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (mode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (mode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isUsageInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
return makeModifierText(modifiers);
|
||||
}
|
||||
|
||||
export function cycleUsageDisplayMode(item: WidgetItem): WidgetItem {
|
||||
const currentMode = getUsageDisplayMode(item);
|
||||
const nextMode: UsageDisplayMode = currentMode === 'time'
|
||||
? 'progress'
|
||||
: currentMode === 'progress'
|
||||
? 'progress-short'
|
||||
: 'time';
|
||||
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleUsageInverted(item: WidgetItem): WidgetItem {
|
||||
return toggleMetadataFlag(item, 'invert');
|
||||
}
|
||||
Reference in New Issue
Block a user