From b001d9cf6ecf6f7b61e051802768291af8246c17 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 5 Aug 2026 21:04:12 -0500 Subject: [PATCH] refactor(usage): extract the usage-percent widgets onto a shared module SessionUsage, WeeklyUsage, WeeklySonnetUsage, WeeklyOpusUsage and FableWeeklyUsage carried five copies of one render body. Normalized for the per-widget names, the worst pair differed by three lines and two pairs were identical. Each file is now delegation, following the speed-widget.tsx and InputSpeed.ts pair. Six axes vary and are parameterized: label, preview percent, usage field, window resolver, display name and description. Default color, category, editor display, editor actions, keybinds and the three supports* flags were already uniform. The resolvers dispatch at call time rather than through a captured reference, because the widget tests spy them on the utils/usage namespace and a module-level table would bind before the spy is installed. FableWeeklyUsage's label was the one of the five that disagreed with its own display name, so 'Fable Weekly: ' becomes 'Weekly Fable: ', matching the sibling rule of display name minus ' Usage'. Raw mode prints no label and is unaffected. The widget type, class name and display name are untouched. ExtraUsageUtilization stays out: it gates on a tri-state enabled flag, has no window resolver, no time cursor and a different default color. The shared suite gains a whole-percent case. formatPercent's format argument is optional and its default reproduces the baseline, so a render path that stopped passing the resolved format rendered identically under default settings and no test could see it. --- src/widgets/FableWeeklyUsage.ts | 118 ++-------- src/widgets/SessionUsage.ts | 116 ++-------- src/widgets/WeeklyOpusUsage.ts | 118 ++-------- src/widgets/WeeklySonnetUsage.ts | 118 ++-------- src/widgets/WeeklyUsage.ts | 116 ++-------- .../__tests__/FableWeeklyUsage.test.ts | 13 +- src/widgets/__tests__/SessionUsage.test.ts | 2 +- src/widgets/__tests__/WeeklyOpusUsage.test.ts | 1 + .../__tests__/WeeklySonnetUsage.test.ts | 1 + src/widgets/__tests__/WeeklyUsage.test.ts | 2 +- .../__tests__/helpers/usage-widget-suites.ts | 16 ++ src/widgets/shared/usage-percent-widget.ts | 207 ++++++++++++++++++ 12 files changed, 299 insertions(+), 529 deletions(-) create mode 100644 src/widgets/shared/usage-percent-widget.ts diff --git a/src/widgets/FableWeeklyUsage.ts b/src/widgets/FableWeeklyUsage.ts index 54aa7912..86dc7834 100644 --- a/src/widgets/FableWeeklyUsage.ts +++ b/src/widgets/FableWeeklyUsage.ts @@ -7,47 +7,27 @@ import type { WidgetEditorDisplay, WidgetItem } from '../types/Widget'; -import { - formatPercent, - resolveNumberFormat -} from '../utils/number-format'; -import { - getUsageErrorMessage, - resolveFableUsageWindow -} from '../utils/usage'; -import { isHidden } from './shared/hideable'; -import { makeTimerProgressBar } from './shared/progress-bar'; -import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, - cycleUsageDisplayMode, - getUsageDisplayMode, - getUsageDisplayModifierText, - getUsagePercentCustomKeybinds, - getUsageProgressBarWidth, - isUsageCursorEnabled, - isUsageInverted, - isUsageProgressMode, - isUsageSliderMode, - makeSliderBar, - toggleUsageCursor, - toggleUsageInverted + getUsagePercentCustomKeybinds } from './shared/usage-display'; - -const LABEL = 'Fable Weekly: '; +import { + getUsagePercentWidgetDescription, + getUsagePercentWidgetDisplayName, + getUsagePercentWidgetEditorDisplay, + handleUsagePercentWidgetEditorAction, + renderUsagePercentWidgetValue +} from './shared/usage-percent-widget'; export class FableWeeklyUsageWidget implements Widget { getDefaultColor(): string { return 'brightBlue'; } - getDescription(): string { return 'Shows Fable-only weekly usage percentage'; } - getDisplayName(): string { return 'Weekly Fable Usage'; } + getDescription(): string { return getUsagePercentWidgetDescription('fable-weekly'); } + getDisplayName(): string { return getUsagePercentWidgetDisplayName('fable-weekly'); } getCategory(): string { return 'Usage'; } getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { - return { - displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) - }; + return getUsagePercentWidgetEditorDisplay('fable-weekly', item); } getHideableStates(): HideableState[] { @@ -55,83 +35,11 @@ export class FableWeeklyUsageWidget implements Widget { } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { - if (action === 'toggle-progress') { - return cycleUsageDisplayMode(item, [], true, true); - } - - if (action === 'toggle-invert') { - return toggleUsageInverted(item); - } - - if (action === 'toggle-cursor') { - return toggleUsageCursor(item); - } - - return null; + return handleUsagePercentWidgetEditorAction(action, item); } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { - const format = resolveNumberFormat('percent', item, settings); - const displayMode = getUsageDisplayMode(item); - const inverted = isUsageInverted(item); - const showCursor = isUsageCursorEnabled(item); - - if (context.isPreview) { - const previewPercent = 4; - const renderedPercent = inverted ? 100 - previewPercent : previewPercent; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - const progressBar = makeTimerProgressBar(renderedPercent, width, showCursor ? { cursorPercent: 50 } : undefined); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); - } - - const data = context.usageData ?? {}; - if (data.fableUsage === undefined) { - if (data.error) { - return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) - ? null - : getUsageErrorMessage(data.error); - } - return null; - } - - const percent = Math.max(0, Math.min(100, data.fableUsage)); - const renderedPercent = inverted ? 100 - percent : percent; - const getCursorOptions = (): { cursorPercent: number } | undefined => { - if (!showCursor) { - return undefined; - } - - const window = resolveFableUsageWindow(data); - return window ? { cursorPercent: window.elapsedPercent } : undefined; - }; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - - const progressBar = makeTimerProgressBar(renderedPercent, width, getCursorOptions()); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); + return renderUsagePercentWidgetValue('fable-weekly', item, context, settings); } getCustomKeybinds(item?: WidgetItem): CustomKeybind[] { diff --git a/src/widgets/SessionUsage.ts b/src/widgets/SessionUsage.ts index 09ceee40..77423134 100644 --- a/src/widgets/SessionUsage.ts +++ b/src/widgets/SessionUsage.ts @@ -7,45 +7,27 @@ import type { WidgetEditorDisplay, WidgetItem } from '../types/Widget'; -import { - formatPercent, - resolveNumberFormat -} from '../utils/number-format'; -import { - getUsageErrorMessage, - resolveUsageWindowWithFallback -} from '../utils/usage'; -import { isHidden } from './shared/hideable'; -import { makeTimerProgressBar } from './shared/progress-bar'; -import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, - cycleUsageDisplayMode, - getUsageDisplayMode, - getUsageDisplayModifierText, - getUsagePercentCustomKeybinds, - getUsageProgressBarWidth, - isUsageCursorEnabled, - isUsageInverted, - isUsageProgressMode, - isUsageSliderMode, - makeSliderBar, - toggleUsageCursor, - toggleUsageInverted + getUsagePercentCustomKeybinds } from './shared/usage-display'; +import { + getUsagePercentWidgetDescription, + getUsagePercentWidgetDisplayName, + getUsagePercentWidgetEditorDisplay, + handleUsagePercentWidgetEditorAction, + renderUsagePercentWidgetValue +} from './shared/usage-percent-widget'; export class SessionUsageWidget implements Widget { getDefaultColor(): string { return 'brightBlue'; } - getDescription(): string { return 'Shows daily/session API usage percentage'; } - getDisplayName(): string { return 'Session Usage'; } + getDescription(): string { return getUsagePercentWidgetDescription('session'); } + getDisplayName(): string { return getUsagePercentWidgetDisplayName('session'); } getCategory(): string { return 'Usage'; } getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { - return { - displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) - }; + return getUsagePercentWidgetEditorDisplay('session', item); } getHideableStates(): HideableState[] { @@ -53,83 +35,11 @@ export class SessionUsageWidget implements Widget { } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { - if (action === 'toggle-progress') { - return cycleUsageDisplayMode(item, [], true, true); - } - - if (action === 'toggle-invert') { - return toggleUsageInverted(item); - } - - if (action === 'toggle-cursor') { - return toggleUsageCursor(item); - } - - return null; + return handleUsagePercentWidgetEditorAction(action, item); } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { - const displayMode = getUsageDisplayMode(item); - const inverted = isUsageInverted(item); - const showCursor = isUsageCursorEnabled(item); - const format = resolveNumberFormat('percent', item, settings); - - if (context.isPreview) { - const previewPercent = 20; - const renderedPercent = inverted ? 100 - previewPercent : previewPercent; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - const progressBar = makeTimerProgressBar(renderedPercent, width, showCursor ? { cursorPercent: 50 } : undefined); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, 'Session: ', progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, 'Session: ', sliderDisplay); - } - - return formatRawOrLabeledValue(item, 'Session: ', formatPercent(renderedPercent, format)); - } - - const data = context.usageData ?? {}; - if (data.sessionUsage === undefined) { - if (data.error) { - return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) - ? null - : getUsageErrorMessage(data.error); - } - return null; - } - - const percent = Math.max(0, Math.min(100, data.sessionUsage)); - const renderedPercent = inverted ? 100 - percent : percent; - const getCursorOptions = (): { cursorPercent: number } | undefined => { - if (!showCursor) { - return undefined; - } - - const window = resolveUsageWindowWithFallback(data, context.blockMetrics); - return window ? { cursorPercent: window.elapsedPercent } : undefined; - }; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - - const progressBar = makeTimerProgressBar(renderedPercent, width, getCursorOptions()); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, 'Session: ', progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, 'Session: ', sliderDisplay); - } - - return formatRawOrLabeledValue(item, 'Session: ', formatPercent(renderedPercent, format)); + return renderUsagePercentWidgetValue('session', item, context, settings); } getCustomKeybinds(item?: WidgetItem): CustomKeybind[] { diff --git a/src/widgets/WeeklyOpusUsage.ts b/src/widgets/WeeklyOpusUsage.ts index 1cec4273..150d0d46 100644 --- a/src/widgets/WeeklyOpusUsage.ts +++ b/src/widgets/WeeklyOpusUsage.ts @@ -7,47 +7,27 @@ import type { WidgetEditorDisplay, WidgetItem } from '../types/Widget'; -import { - formatPercent, - resolveNumberFormat -} from '../utils/number-format'; -import { - getUsageErrorMessage, - resolveWeeklyOpusUsageWindow -} from '../utils/usage'; -import { isHidden } from './shared/hideable'; -import { makeTimerProgressBar } from './shared/progress-bar'; -import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, - cycleUsageDisplayMode, - getUsageDisplayMode, - getUsageDisplayModifierText, - getUsagePercentCustomKeybinds, - getUsageProgressBarWidth, - isUsageCursorEnabled, - isUsageInverted, - isUsageProgressMode, - isUsageSliderMode, - makeSliderBar, - toggleUsageCursor, - toggleUsageInverted + getUsagePercentCustomKeybinds } from './shared/usage-display'; - -const LABEL = 'Weekly Opus: '; +import { + getUsagePercentWidgetDescription, + getUsagePercentWidgetDisplayName, + getUsagePercentWidgetEditorDisplay, + handleUsagePercentWidgetEditorAction, + renderUsagePercentWidgetValue +} from './shared/usage-percent-widget'; export class WeeklyOpusUsageWidget implements Widget { getDefaultColor(): string { return 'brightBlue'; } - getDescription(): string { return 'Shows weekly Opus API usage percentage'; } - getDisplayName(): string { return 'Weekly Opus Usage'; } + getDescription(): string { return getUsagePercentWidgetDescription('weekly-opus'); } + getDisplayName(): string { return getUsagePercentWidgetDisplayName('weekly-opus'); } getCategory(): string { return 'Usage'; } getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { - return { - displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) - }; + return getUsagePercentWidgetEditorDisplay('weekly-opus', item); } getHideableStates(): HideableState[] { @@ -55,83 +35,11 @@ export class WeeklyOpusUsageWidget implements Widget { } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { - if (action === 'toggle-progress') { - return cycleUsageDisplayMode(item, [], true, true); - } - - if (action === 'toggle-invert') { - return toggleUsageInverted(item); - } - - if (action === 'toggle-cursor') { - return toggleUsageCursor(item); - } - - return null; + return handleUsagePercentWidgetEditorAction(action, item); } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { - const displayMode = getUsageDisplayMode(item); - const inverted = isUsageInverted(item); - const showCursor = isUsageCursorEnabled(item); - const format = resolveNumberFormat('percent', item, settings); - - if (context.isPreview) { - const previewPercent = 4; - const renderedPercent = inverted ? 100 - previewPercent : previewPercent; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - const progressBar = makeTimerProgressBar(renderedPercent, width, showCursor ? { cursorPercent: 50 } : undefined); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); - } - - const data = context.usageData ?? {}; - if (data.weeklyOpusUsage === undefined) { - if (data.error) { - return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) - ? null - : getUsageErrorMessage(data.error); - } - return null; - } - - const percent = Math.max(0, Math.min(100, data.weeklyOpusUsage)); - const renderedPercent = inverted ? 100 - percent : percent; - const getCursorOptions = (): { cursorPercent: number } | undefined => { - if (!showCursor) { - return undefined; - } - - const window = resolveWeeklyOpusUsageWindow(data); - return window ? { cursorPercent: window.elapsedPercent } : undefined; - }; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - - const progressBar = makeTimerProgressBar(renderedPercent, width, getCursorOptions()); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); + return renderUsagePercentWidgetValue('weekly-opus', item, context, settings); } getCustomKeybinds(item?: WidgetItem): CustomKeybind[] { diff --git a/src/widgets/WeeklySonnetUsage.ts b/src/widgets/WeeklySonnetUsage.ts index e35bc215..96eff8cc 100644 --- a/src/widgets/WeeklySonnetUsage.ts +++ b/src/widgets/WeeklySonnetUsage.ts @@ -7,47 +7,27 @@ import type { WidgetEditorDisplay, WidgetItem } from '../types/Widget'; -import { - formatPercent, - resolveNumberFormat -} from '../utils/number-format'; -import { - getUsageErrorMessage, - resolveWeeklySonnetUsageWindow -} from '../utils/usage'; -import { isHidden } from './shared/hideable'; -import { makeTimerProgressBar } from './shared/progress-bar'; -import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, - cycleUsageDisplayMode, - getUsageDisplayMode, - getUsageDisplayModifierText, - getUsagePercentCustomKeybinds, - getUsageProgressBarWidth, - isUsageCursorEnabled, - isUsageInverted, - isUsageProgressMode, - isUsageSliderMode, - makeSliderBar, - toggleUsageCursor, - toggleUsageInverted + getUsagePercentCustomKeybinds } from './shared/usage-display'; - -const LABEL = 'Weekly Sonnet: '; +import { + getUsagePercentWidgetDescription, + getUsagePercentWidgetDisplayName, + getUsagePercentWidgetEditorDisplay, + handleUsagePercentWidgetEditorAction, + renderUsagePercentWidgetValue +} from './shared/usage-percent-widget'; export class WeeklySonnetUsageWidget implements Widget { getDefaultColor(): string { return 'brightBlue'; } - getDescription(): string { return 'Shows weekly Sonnet API usage percentage'; } - getDisplayName(): string { return 'Weekly Sonnet Usage'; } + getDescription(): string { return getUsagePercentWidgetDescription('weekly-sonnet'); } + getDisplayName(): string { return getUsagePercentWidgetDisplayName('weekly-sonnet'); } getCategory(): string { return 'Usage'; } getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { - return { - displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) - }; + return getUsagePercentWidgetEditorDisplay('weekly-sonnet', item); } getHideableStates(): HideableState[] { @@ -55,83 +35,11 @@ export class WeeklySonnetUsageWidget implements Widget { } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { - if (action === 'toggle-progress') { - return cycleUsageDisplayMode(item, [], true, true); - } - - if (action === 'toggle-invert') { - return toggleUsageInverted(item); - } - - if (action === 'toggle-cursor') { - return toggleUsageCursor(item); - } - - return null; + return handleUsagePercentWidgetEditorAction(action, item); } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { - const displayMode = getUsageDisplayMode(item); - const inverted = isUsageInverted(item); - const showCursor = isUsageCursorEnabled(item); - const format = resolveNumberFormat('percent', item, settings); - - if (context.isPreview) { - const previewPercent = 8; - const renderedPercent = inverted ? 100 - previewPercent : previewPercent; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - const progressBar = makeTimerProgressBar(renderedPercent, width, showCursor ? { cursorPercent: 50 } : undefined); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); - } - - const data = context.usageData ?? {}; - if (data.weeklySonnetUsage === undefined) { - if (data.error) { - return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) - ? null - : getUsageErrorMessage(data.error); - } - return null; - } - - const percent = Math.max(0, Math.min(100, data.weeklySonnetUsage)); - const renderedPercent = inverted ? 100 - percent : percent; - const getCursorOptions = (): { cursorPercent: number } | undefined => { - if (!showCursor) { - return undefined; - } - - const window = resolveWeeklySonnetUsageWindow(data); - return window ? { cursorPercent: window.elapsedPercent } : undefined; - }; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - - const progressBar = makeTimerProgressBar(renderedPercent, width, getCursorOptions()); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, LABEL, progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, LABEL, sliderDisplay); - } - - return formatRawOrLabeledValue(item, LABEL, formatPercent(renderedPercent, format)); + return renderUsagePercentWidgetValue('weekly-sonnet', item, context, settings); } getCustomKeybinds(item?: WidgetItem): CustomKeybind[] { diff --git a/src/widgets/WeeklyUsage.ts b/src/widgets/WeeklyUsage.ts index 2269502f..86544d55 100644 --- a/src/widgets/WeeklyUsage.ts +++ b/src/widgets/WeeklyUsage.ts @@ -7,45 +7,27 @@ import type { WidgetEditorDisplay, WidgetItem } from '../types/Widget'; -import { - formatPercent, - resolveNumberFormat -} from '../utils/number-format'; -import { - getUsageErrorMessage, - resolveWeeklyUsageWindow -} from '../utils/usage'; -import { isHidden } from './shared/hideable'; -import { makeTimerProgressBar } from './shared/progress-bar'; -import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, - cycleUsageDisplayMode, - getUsageDisplayMode, - getUsageDisplayModifierText, - getUsagePercentCustomKeybinds, - getUsageProgressBarWidth, - isUsageCursorEnabled, - isUsageInverted, - isUsageProgressMode, - isUsageSliderMode, - makeSliderBar, - toggleUsageCursor, - toggleUsageInverted + getUsagePercentCustomKeybinds } from './shared/usage-display'; +import { + getUsagePercentWidgetDescription, + getUsagePercentWidgetDisplayName, + getUsagePercentWidgetEditorDisplay, + handleUsagePercentWidgetEditorAction, + renderUsagePercentWidgetValue +} from './shared/usage-percent-widget'; export class WeeklyUsageWidget implements Widget { getDefaultColor(): string { return 'brightBlue'; } - getDescription(): string { return 'Shows weekly API usage percentage'; } - getDisplayName(): string { return 'Weekly Usage'; } + getDescription(): string { return getUsagePercentWidgetDescription('weekly'); } + getDisplayName(): string { return getUsagePercentWidgetDisplayName('weekly'); } getCategory(): string { return 'Usage'; } getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { - return { - displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) - }; + return getUsagePercentWidgetEditorDisplay('weekly', item); } getHideableStates(): HideableState[] { @@ -53,83 +35,11 @@ export class WeeklyUsageWidget implements Widget { } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { - if (action === 'toggle-progress') { - return cycleUsageDisplayMode(item, [], true, true); - } - - if (action === 'toggle-invert') { - return toggleUsageInverted(item); - } - - if (action === 'toggle-cursor') { - return toggleUsageCursor(item); - } - - return null; + return handleUsagePercentWidgetEditorAction(action, item); } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { - const displayMode = getUsageDisplayMode(item); - const inverted = isUsageInverted(item); - const showCursor = isUsageCursorEnabled(item); - const format = resolveNumberFormat('percent', item, settings); - - if (context.isPreview) { - const previewPercent = 12; - const renderedPercent = inverted ? 100 - previewPercent : previewPercent; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - const progressBar = makeTimerProgressBar(renderedPercent, width, showCursor ? { cursorPercent: 50 } : undefined); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, 'Weekly: ', progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, 'Weekly: ', sliderDisplay); - } - - return formatRawOrLabeledValue(item, 'Weekly: ', formatPercent(renderedPercent, format)); - } - - const data = context.usageData ?? {}; - if (data.weeklyUsage === undefined) { - if (data.error) { - return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) - ? null - : getUsageErrorMessage(data.error); - } - return null; - } - - const percent = Math.max(0, Math.min(100, data.weeklyUsage)); - const renderedPercent = inverted ? 100 - percent : percent; - const getCursorOptions = (): { cursorPercent: number } | undefined => { - if (!showCursor) { - return undefined; - } - - const window = resolveWeeklyUsageWindow(data); - return window ? { cursorPercent: window.elapsedPercent } : undefined; - }; - - if (isUsageProgressMode(displayMode)) { - const width = getUsageProgressBarWidth(displayMode); - - const progressBar = makeTimerProgressBar(renderedPercent, width, getCursorOptions()); - const progressDisplay = `[${progressBar}] ${formatPercent(renderedPercent, format)}`; - return formatRawOrLabeledValue(item, 'Weekly: ', progressDisplay); - } - - if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); - const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(renderedPercent, format)}` : slider; - return formatRawOrLabeledValue(item, 'Weekly: ', sliderDisplay); - } - - return formatRawOrLabeledValue(item, 'Weekly: ', formatPercent(renderedPercent, format)); + return renderUsagePercentWidgetValue('weekly', item, context, settings); } getCustomKeybinds(item?: WidgetItem): CustomKeybind[] { diff --git a/src/widgets/__tests__/FableWeeklyUsage.test.ts b/src/widgets/__tests__/FableWeeklyUsage.test.ts index dfb229e1..7f7dc381 100644 --- a/src/widgets/__tests__/FableWeeklyUsage.test.ts +++ b/src/widgets/__tests__/FableWeeklyUsage.test.ts @@ -55,12 +55,12 @@ describe('FableWeeklyUsageWidget', () => { id: 'fable-weekly', type: 'fable-weekly-usage', metadata: { cursor: 'true', display: 'slider' } - }, context)).toBe('Fable Weekly: ▓▓░░░│░░░░ 20.0%'); + }, context)).toBe('Weekly Fable: ▓▓░░░│░░░░ 20.0%'); expect(render(widget, { id: 'fable-weekly', type: 'fable-weekly-usage', metadata: { cursor: 'true', display: 'slider-only' } - }, context)).toBe('Fable Weekly: ▓▓░░░│░░░░'); + }, context)).toBe('Weekly Fable: ▓▓░░░│░░░░'); }); it('returns null when the per-model usage is missing from the API response', () => { @@ -72,14 +72,15 @@ describe('FableWeeklyUsageWidget', () => { baseItem: { id: 'fable-weekly', type: 'fable-weekly-usage' }, createWidget: () => new FableWeeklyUsageWidget(), errorMessageMock: usageErrorMessageMock, - expectedInvertedTime: 'Fable Weekly: 57.9%', + expectedInvertedTime: 'Weekly Fable: 57.9%', expectedModifierText: '(long bar, remaining)', - expectedPreviewInvertedTime: 'Fable Weekly: 96.0%', - expectedProgress: 'Fable Weekly: [███████████████████░░░░░░░░░░░░░] 57.9%', + expectedPreviewInvertedTime: 'Weekly Fable: 96.0%', + expectedProgress: 'Weekly Fable: [███████████████████░░░░░░░░░░░░░] 57.9%', expectedRawInvertedTime: '57.9%', expectedRawProgress: '[███████░░░░░░░░░] 42.1%', expectedRawTime: '42.1%', - expectedTime: 'Fable Weekly: 42.1%', + expectedTime: 'Weekly Fable: 42.1%', + expectedWholePercentTime: 'Weekly Fable: 42%', modifierItem: { id: 'fable-weekly', type: 'fable-weekly-usage', diff --git a/src/widgets/__tests__/SessionUsage.test.ts b/src/widgets/__tests__/SessionUsage.test.ts index 6d7b168b..bec6dc83 100644 --- a/src/widgets/__tests__/SessionUsage.test.ts +++ b/src/widgets/__tests__/SessionUsage.test.ts @@ -39,7 +39,6 @@ describe('SessionUsageWidget', () => { beforeEach(() => { vi.restoreAllMocks(); mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage'); - // makeUsageProgressBar no longer used; SessionUsage uses makeTimerProgressBar directly }); afterEach(() => { @@ -76,6 +75,7 @@ describe('SessionUsageWidget', () => { expectedRawProgress: '[████████░░░░░░░░░░░░░░░░░░░░░░░░] 23.4%', expectedRawTime: '23.4%', expectedTime: 'Session: 23.4%', + expectedWholePercentTime: 'Session: 23%', modifierItem: { id: 'session', type: 'session-usage', diff --git a/src/widgets/__tests__/WeeklyOpusUsage.test.ts b/src/widgets/__tests__/WeeklyOpusUsage.test.ts index 8c9d128f..ddd257c7 100644 --- a/src/widgets/__tests__/WeeklyOpusUsage.test.ts +++ b/src/widgets/__tests__/WeeklyOpusUsage.test.ts @@ -80,6 +80,7 @@ describe('WeeklyOpusUsageWidget', () => { expectedRawProgress: '[███████░░░░░░░░░] 42.1%', expectedRawTime: '42.1%', expectedTime: 'Weekly Opus: 42.1%', + expectedWholePercentTime: 'Weekly Opus: 42%', modifierItem: { id: 'weekly-opus', type: 'weekly-opus-usage', diff --git a/src/widgets/__tests__/WeeklySonnetUsage.test.ts b/src/widgets/__tests__/WeeklySonnetUsage.test.ts index ba94cb79..6c51c5b2 100644 --- a/src/widgets/__tests__/WeeklySonnetUsage.test.ts +++ b/src/widgets/__tests__/WeeklySonnetUsage.test.ts @@ -80,6 +80,7 @@ describe('WeeklySonnetUsageWidget', () => { expectedRawProgress: '[███████░░░░░░░░░] 42.1%', expectedRawTime: '42.1%', expectedTime: 'Weekly Sonnet: 42.1%', + expectedWholePercentTime: 'Weekly Sonnet: 42%', modifierItem: { id: 'weekly-sonnet', type: 'weekly-sonnet-usage', diff --git a/src/widgets/__tests__/WeeklyUsage.test.ts b/src/widgets/__tests__/WeeklyUsage.test.ts index 7bb76c68..ae3e3571 100644 --- a/src/widgets/__tests__/WeeklyUsage.test.ts +++ b/src/widgets/__tests__/WeeklyUsage.test.ts @@ -39,7 +39,6 @@ describe('WeeklyUsageWidget', () => { beforeEach(() => { vi.restoreAllMocks(); mockGetUsageErrorMessage = vi.spyOn(usage, 'getUsageErrorMessage'); - // makeUsageProgressBar no longer used; WeeklyUsage uses makeTimerProgressBar directly }); afterEach(() => { @@ -76,6 +75,7 @@ describe('WeeklyUsageWidget', () => { expectedRawProgress: '[███████░░░░░░░░░] 42.1%', expectedRawTime: '42.1%', expectedTime: 'Weekly: 42.1%', + expectedWholePercentTime: 'Weekly: 42%', modifierItem: { id: 'weekly', type: 'weekly-usage', diff --git a/src/widgets/__tests__/helpers/usage-widget-suites.ts b/src/widgets/__tests__/helpers/usage-widget-suites.ts index 5b375375..9ec6790a 100644 --- a/src/widgets/__tests__/helpers/usage-widget-suites.ts +++ b/src/widgets/__tests__/helpers/usage-widget-suites.ts @@ -31,6 +31,7 @@ interface UsagePercentWidgetSuiteConfig { expectedRawTime: string; expectedInvertedTime: string; expectedTime: string; + expectedWholePercentTime: string; modifierItem: WidgetItem; progressItem: WidgetItem; rawProgressItem: WidgetItem; @@ -160,6 +161,21 @@ export function runUsagePercentWidgetSuite(conf expect(config.render(widget, config.baseItem, context)).toBe(config.expectedTime); }); + // formatPercent's format argument is optional and its default reproduces the + // baseline output, so a render path that stops passing the resolved format + // stays invisible against default settings. Pinning a non-default style is + // what makes that reachable. + it('applies the resolved number format to the percentage', () => { + const widget = config.createWidget(); + const context = getUsageContext(config.usageField, config.usageValue); + const wholePercentItem: WidgetItem = { + ...config.baseItem, + numberFormat: { style: 'whole' } + }; + + expect(config.render(widget, wholePercentItem, context)).toBe(config.expectedWholePercentTime); + }); + it('renders inverted percentage in time mode', () => { const widget = config.createWidget(); const context = getUsageContext(config.usageField, config.usageValue); diff --git a/src/widgets/shared/usage-percent-widget.ts b/src/widgets/shared/usage-percent-widget.ts new file mode 100644 index 00000000..8e12728e --- /dev/null +++ b/src/widgets/shared/usage-percent-widget.ts @@ -0,0 +1,207 @@ +import type { NumberFormat } from '../../types/NumberFormat'; +import type { + RenderContext, + RenderUsageData +} from '../../types/RenderContext'; +import type { Settings } from '../../types/Settings'; +import type { + WidgetEditorDisplay, + WidgetItem +} from '../../types/Widget'; +import { + formatPercent, + resolveNumberFormat +} from '../../utils/number-format'; +import { + getUsageErrorMessage, + resolveFableUsageWindow, + resolveUsageWindowWithFallback, + resolveWeeklyOpusUsageWindow, + resolveWeeklySonnetUsageWindow, + resolveWeeklyUsageWindow +} from '../../utils/usage'; +import type { UsageWindowMetrics } from '../../utils/usage-types'; + +import { isHidden } from './hideable'; +import { makeTimerProgressBar } from './progress-bar'; +import { formatRawOrLabeledValue } from './raw-or-labeled'; +import { + USAGE_NO_DATA_HIDEABLE_STATE, + cycleUsageDisplayMode, + getUsageDisplayMode, + getUsageDisplayModifierText, + getUsageProgressBarWidth, + isUsageCursorEnabled, + isUsageInverted, + isUsageProgressMode, + isUsageSliderMode, + makeSliderBar, + toggleUsageCursor, + toggleUsageInverted +} from './usage-display'; + +export type UsagePercentWidgetKind = 'session' | 'weekly' | 'weekly-sonnet' | 'weekly-opus' | 'fable-weekly'; + +type UsagePercentField = 'sessionUsage' | 'weeklyUsage' | 'weeklySonnetUsage' | 'weeklyOpusUsage' | 'fableUsage'; + +interface UsageCursorOptions { cursorPercent: number } + +interface UsagePercentWidgetKindConfig { + label: string; + displayName: string; + description: string; + previewPercent: number; + usageField: UsagePercentField; +} + +const USAGE_PERCENT_WIDGET_CONFIG: Record = { + 'session': { + label: 'Session: ', + displayName: 'Session Usage', + description: 'Shows daily/session API usage percentage', + previewPercent: 20, + usageField: 'sessionUsage' + }, + 'weekly': { + label: 'Weekly: ', + displayName: 'Weekly Usage', + description: 'Shows weekly API usage percentage', + previewPercent: 12, + usageField: 'weeklyUsage' + }, + 'weekly-sonnet': { + label: 'Weekly Sonnet: ', + displayName: 'Weekly Sonnet Usage', + description: 'Shows weekly Sonnet API usage percentage', + previewPercent: 8, + usageField: 'weeklySonnetUsage' + }, + 'weekly-opus': { + label: 'Weekly Opus: ', + displayName: 'Weekly Opus Usage', + description: 'Shows weekly Opus API usage percentage', + previewPercent: 4, + usageField: 'weeklyOpusUsage' + }, + 'fable-weekly': { + label: 'Weekly Fable: ', + displayName: 'Weekly Fable Usage', + description: 'Shows Fable-only weekly usage percentage', + previewPercent: 4, + usageField: 'fableUsage' + } +}; + +// The session window also consults block metrics, so the resolvers take different +// arguments and are picked per call rather than stored alongside the config. +function resolveUsageWindow(kind: UsagePercentWidgetKind, data: RenderUsageData, context: RenderContext): UsageWindowMetrics | null { + if (kind === 'session') { + return resolveUsageWindowWithFallback(data, context.blockMetrics); + } + if (kind === 'weekly') { + return resolveWeeklyUsageWindow(data); + } + if (kind === 'weekly-sonnet') { + return resolveWeeklySonnetUsageWindow(data); + } + if (kind === 'weekly-opus') { + return resolveWeeklyOpusUsageWindow(data); + } + return resolveFableUsageWindow(data); +} + +function renderUsageDisplay( + item: WidgetItem, + label: string, + percent: number, + format: NumberFormat, + getCursorOptions: () => UsageCursorOptions | undefined +): string { + const displayMode = getUsageDisplayMode(item); + + if (isUsageProgressMode(displayMode)) { + const width = getUsageProgressBarWidth(displayMode); + const progressBar = makeTimerProgressBar(percent, width, getCursorOptions()); + const progressDisplay = `[${progressBar}] ${formatPercent(percent, format)}`; + return formatRawOrLabeledValue(item, label, progressDisplay); + } + + if (isUsageSliderMode(displayMode)) { + const slider = makeSliderBar(percent, undefined, getCursorOptions()); + const sliderDisplay = displayMode === 'slider' ? `${slider} ${formatPercent(percent, format)}` : slider; + return formatRawOrLabeledValue(item, label, sliderDisplay); + } + + return formatRawOrLabeledValue(item, label, formatPercent(percent, format)); +} + +export function getUsagePercentWidgetDisplayName(kind: UsagePercentWidgetKind): string { + return USAGE_PERCENT_WIDGET_CONFIG[kind].displayName; +} + +export function getUsagePercentWidgetDescription(kind: UsagePercentWidgetKind): string { + return USAGE_PERCENT_WIDGET_CONFIG[kind].description; +} + +export function getUsagePercentWidgetEditorDisplay(kind: UsagePercentWidgetKind, item: WidgetItem): WidgetEditorDisplay { + return { + displayText: getUsagePercentWidgetDisplayName(kind), + modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true }) + }; +} + +export function handleUsagePercentWidgetEditorAction(action: string, item: WidgetItem): WidgetItem | null { + if (action === 'toggle-progress') { + return cycleUsageDisplayMode(item, [], true, true); + } + + if (action === 'toggle-invert') { + return toggleUsageInverted(item); + } + + if (action === 'toggle-cursor') { + return toggleUsageCursor(item); + } + + return null; +} + +export function renderUsagePercentWidgetValue( + kind: UsagePercentWidgetKind, + item: WidgetItem, + context: RenderContext, + settings: Settings +): string | null { + const config = USAGE_PERCENT_WIDGET_CONFIG[kind]; + const inverted = isUsageInverted(item); + const showCursor = isUsageCursorEnabled(item); + const format = resolveNumberFormat('percent', item, settings); + + if (context.isPreview) { + const renderedPercent = inverted ? 100 - config.previewPercent : config.previewPercent; + return renderUsageDisplay(item, config.label, renderedPercent, format, () => showCursor ? { cursorPercent: 50 } : undefined); + } + + const data: RenderUsageData = context.usageData ?? {}; + const usagePercent = data[config.usageField]; + if (usagePercent === undefined) { + if (data.error) { + return isHidden(item, USAGE_NO_DATA_HIDEABLE_STATE.key) + ? null + : getUsageErrorMessage(data.error); + } + return null; + } + + const percent = Math.max(0, Math.min(100, usagePercent)); + const renderedPercent = inverted ? 100 - percent : percent; + + return renderUsageDisplay(item, config.label, renderedPercent, format, () => { + if (!showCursor) { + return undefined; + } + + const window = resolveUsageWindow(kind, data, context); + return window ? { cursorPercent: window.elapsedPercent } : undefined; + }); +}