diff --git a/docs/USAGE.md b/docs/USAGE.md index 7f8ac111a..28798ba98 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -337,13 +337,13 @@ Supported states by widget family: - **Session Cost**: `zero` hides `$0.00` - **Session Clock**: `zero` hides durations under one minute - **Block Timer**: `no-data` hides the `0hr 0m` / empty-bar display when no block is active -- **Block Reset Timer / Weekly Reset Timer**: `no-data` hides both the `[Loading]` placeholder and the usage-error placeholders while no reset window is available +- **Block Reset Timer / Weekly Reset Timer**: `no-data` hides both the `[Loading]` placeholder and the usage-error placeholders while no reset window is available, `zero` hides the timer while its period's usage (session or weekly) is 0% and no reset window has started - **Input/Output/Total Speed**: `no-data` hides the `—` placeholder when no speed data exists - **Output Style**: `default-value` hides the widget when the style is `default` - **Compaction Counter**: `zero` hides the counter before any compaction occurs - **Skills**: `empty` hides the widget before any skill is used -- **Extra Usage widgets**: `disabled` hides the `n/a` display when extra usage is off, `no-data` hides the error placeholder when usage data is unavailable -- **Session / Weekly / Weekly Sonnet / Weekly Opus / Weekly Fable Usage**: `no-data` hides the error placeholder (`[No credentials]`, `[Timeout]`, `[Rate limited]`, `[API Error]`, `[Parse Error]`) when usage data is unavailable +- **Extra Usage widgets**: `disabled` hides the `n/a` display when extra usage is off, `no-data` hides the error placeholder when usage data is unavailable; on Extra Usage Used and Extra Usage Utilization, `zero` hides the widget while nothing has been spent (Extra Usage Remaining has no `zero` state) +- **Session / Weekly / Weekly Sonnet / Weekly Opus / Weekly Fable Usage**: `no-data` hides the error placeholder (`[No credentials]`, `[Timeout]`, `[Rate limited]`, `[API Error]`, `[Parse Error]`) when usage data is unavailable, `zero` hides the widget while usage is 0% (also when showing remaining) - **Cache widgets** (`Cache Hit Rate`, `Cache Read`, `Cache Write`, `Cache Timer`): `empty` hides the widget when there is no cache activity, and on Cache Timer when no cache anchor is available - **Custom Text / Custom Symbol**: `merge-target-hidden` hides the item when the widget it is merged with renders nothing, so icon prefixes/suffixes disappear together with their widget diff --git a/src/utils/migrations.ts b/src/utils/migrations.ts index d556d2cb3..8eb226389 100644 --- a/src/utils/migrations.ts +++ b/src/utils/migrations.ts @@ -161,6 +161,8 @@ const GIT_REVIEW_HIDE_RULE: HideFlagRule = { // placeholder. hideIfDisabled never covered that placeholder, so it stays mapped // to 'disabled' alone and only a hand-written list can enable it. const EXTRA_USAGE_HIDE_RULE: HideFlagRule = { legacy: { hideIfDisabled: ['disabled'] }, stateOrder: ['disabled', 'no-data'] }; +// Used and Utilization also offer `zero`, which a hand-written hide list can carry in. +const EXTRA_USAGE_ZERO_HIDE_RULE: HideFlagRule = { legacy: { hideIfDisabled: ['disabled'] }, stateOrder: ['disabled', 'no-data', 'zero'] }; const CACHE_HIDE_RULE: HideFlagRule = { legacy: { hideWhenEmpty: ['empty'] }, stateOrder: ['empty'] }; export const V4_HIDE_FLAG_RULES: Record = { @@ -209,9 +211,9 @@ export const V4_HIDE_FLAG_RULES: Record = { 'cache-write': CACHE_HIDE_RULE, 'cache-hit-rate': CACHE_HIDE_RULE, 'cache-timer': CACHE_HIDE_RULE, - 'extra-usage-utilization': EXTRA_USAGE_HIDE_RULE, + 'extra-usage-utilization': EXTRA_USAGE_ZERO_HIDE_RULE, 'extra-usage-remaining': EXTRA_USAGE_HIDE_RULE, - 'extra-usage-used': EXTRA_USAGE_HIDE_RULE + 'extra-usage-used': EXTRA_USAGE_ZERO_HIDE_RULE }; const V4_LEGACY_HIDE_KEYS = [ diff --git a/src/widgets/BlockResetTimer.ts b/src/widgets/BlockResetTimer.ts index 15575c88d..040f415eb 100644 --- a/src/widgets/BlockResetTimer.ts +++ b/src/widgets/BlockResetTimer.ts @@ -34,6 +34,7 @@ import { } from './shared/timezone-editor'; import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, cycleUsageDisplayMode, getUsageDisplayMode, getUsageDisplayModifierText, @@ -78,7 +79,7 @@ export class BlockResetTimerWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { @@ -144,6 +145,10 @@ export class BlockResetTimerWidget implements Widget { } const usageData = context.usageData ?? {}; + if (usageData.sessionUsage === 0 && !usageData.sessionResetAt && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) { + return null; + } + const window = resolveUsageWindowWithFallback(usageData, context.blockMetrics); if (!window) { diff --git a/src/widgets/ExtraUsageUsed.ts b/src/widgets/ExtraUsageUsed.ts index acf19c714..87ad2fe53 100644 --- a/src/widgets/ExtraUsageUsed.ts +++ b/src/widgets/ExtraUsageUsed.ts @@ -1,6 +1,16 @@ -import type { RenderUsageData } from '../types/RenderContext'; +import type { + RenderContext, + RenderUsageData +} from '../types/RenderContext'; +import type { Settings } from '../types/Settings'; +import type { + HideableState, + WidgetItem +} from '../types/Widget'; import { ExtraUsageAmountWidget } from './shared/extra-usage-amount-widget'; +import { isHidden } from './shared/hideable'; +import { USAGE_ZERO_HIDEABLE_STATE } from './shared/usage-display'; export class ExtraUsageUsedWidget extends ExtraUsageAmountWidget { protected readonly label = 'Overage Used: '; @@ -9,6 +19,18 @@ export class ExtraUsageUsedWidget extends ExtraUsageAmountWidget { getDescription(): string { return 'Shows extra usage spent: overage beyond Pro/Max plan limits, or your spend on Enterprise'; } getDisplayName(): string { return 'Extra Usage Used'; } + override getHideableStates(): HideableState[] { + return [...super.getHideableStates(), USAGE_ZERO_HIDEABLE_STATE]; + } + + override render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { + const data = context.usageData; + if (!context.isPreview && data?.extraUsageEnabled === true && data.extraUsageUsed === 0 && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) { + return null; + } + return super.render(item, context, settings); + } + protected getDollars(data: RenderUsageData): number | null { // extraUsageUsed is in cents return data.extraUsageUsed === undefined ? null : data.extraUsageUsed / 100; diff --git a/src/widgets/ExtraUsageUtilization.ts b/src/widgets/ExtraUsageUtilization.ts index da4916fd6..1cb3e1e1f 100644 --- a/src/widgets/ExtraUsageUtilization.ts +++ b/src/widgets/ExtraUsageUtilization.ts @@ -22,6 +22,7 @@ import { makeTimerProgressBar } from './shared/progress-bar'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, cycleUsageDisplayMode, getUsageDisplayMode, getUsageDisplayModifierText, @@ -62,7 +63,7 @@ export class ExtraUsageUtilizationWidget implements Widget { } getHideableStates(): HideableState[] { - return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE]; + return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { @@ -119,6 +120,10 @@ export class ExtraUsageUtilizationWidget implements Widget { // utilization is a percentage (0-100), not a fraction const percent = Math.max(0, Math.min(100, utilization)); + if (percent === 0 && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) { + return null; + } + const renderedPercent = inverted ? 100 - percent : percent; if (isUsageProgressMode(displayMode)) { diff --git a/src/widgets/FableWeeklyUsage.ts b/src/widgets/FableWeeklyUsage.ts index 6bc331635..e0884eb19 100644 --- a/src/widgets/FableWeeklyUsage.ts +++ b/src/widgets/FableWeeklyUsage.ts @@ -10,6 +10,7 @@ import type { import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, getUsagePercentCustomKeybinds } from './shared/usage-display'; import { @@ -33,7 +34,7 @@ export class FableWeeklyUsageWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { diff --git a/src/widgets/SessionUsage.ts b/src/widgets/SessionUsage.ts index e3b80f596..a0ae35e8e 100644 --- a/src/widgets/SessionUsage.ts +++ b/src/widgets/SessionUsage.ts @@ -10,6 +10,7 @@ import type { import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, getUsagePercentCustomKeybinds } from './shared/usage-display'; import { @@ -33,7 +34,7 @@ export class SessionUsageWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { diff --git a/src/widgets/WeeklyOpusUsage.ts b/src/widgets/WeeklyOpusUsage.ts index 5c5fca343..3513d682b 100644 --- a/src/widgets/WeeklyOpusUsage.ts +++ b/src/widgets/WeeklyOpusUsage.ts @@ -10,6 +10,7 @@ import type { import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, getUsagePercentCustomKeybinds } from './shared/usage-display'; import { @@ -33,7 +34,7 @@ export class WeeklyOpusUsageWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { diff --git a/src/widgets/WeeklyResetTimer.ts b/src/widgets/WeeklyResetTimer.ts index 8803c20a1..b1dc9c58d 100644 --- a/src/widgets/WeeklyResetTimer.ts +++ b/src/widgets/WeeklyResetTimer.ts @@ -39,6 +39,7 @@ import { } from './shared/timezone-editor'; import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, cycleUsageDisplayMode, getUsageDisplayMode, getUsageLocale, @@ -148,7 +149,7 @@ export class WeeklyResetTimerWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { @@ -228,6 +229,10 @@ export class WeeklyResetTimerWidget implements Widget { } const usageData = context.usageData ?? {}; + if (usageData.weeklyUsage === 0 && !usageData.weeklyResetAt && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) { + return null; + } + const window = resolveWeeklyUsageWindow(usageData); if (!window) { diff --git a/src/widgets/WeeklySonnetUsage.ts b/src/widgets/WeeklySonnetUsage.ts index d16406183..8e3dc3054 100644 --- a/src/widgets/WeeklySonnetUsage.ts +++ b/src/widgets/WeeklySonnetUsage.ts @@ -10,6 +10,7 @@ import type { import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, getUsagePercentCustomKeybinds } from './shared/usage-display'; import { @@ -33,7 +34,7 @@ export class WeeklySonnetUsageWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { diff --git a/src/widgets/WeeklyUsage.ts b/src/widgets/WeeklyUsage.ts index 7e08d5360..fcd29bfd6 100644 --- a/src/widgets/WeeklyUsage.ts +++ b/src/widgets/WeeklyUsage.ts @@ -10,6 +10,7 @@ import type { import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, getUsagePercentCustomKeybinds } from './shared/usage-display'; import { @@ -33,7 +34,7 @@ export class WeeklyUsageWidget implements Widget { } getHideableStates(): HideableState[] { - return [USAGE_NO_DATA_HIDEABLE_STATE]; + return [USAGE_NO_DATA_HIDEABLE_STATE, USAGE_ZERO_HIDEABLE_STATE]; } handleEditorAction(action: string, item: WidgetItem): WidgetItem | null { diff --git a/src/widgets/__tests__/BlockResetTimer.test.ts b/src/widgets/__tests__/BlockResetTimer.test.ts index 7f1e03ce6..f7fd065f8 100644 --- a/src/widgets/__tests__/BlockResetTimer.test.ts +++ b/src/widgets/__tests__/BlockResetTimer.test.ts @@ -120,8 +120,28 @@ describe('BlockResetTimerWidget', () => { expect(render(widget, { id: 'reset', type: 'reset-timer', metadata: { display: 'progress' } }, { usageData: {} })).toBe('Reset [Loading]'); }); - it('declares the no-data hideable state', () => { - expect(new BlockResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data']); + it('declares the no-data and zero hideable states', () => { + expect(new BlockResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data', 'zero']); + }); + + it('hides when sessionUsage is zero and the zero state is enabled', () => { + const widget = new BlockResetTimerWidget(); + const item: WidgetItem = { id: 'reset', type: 'reset-timer', metadata: { hide: 'zero' } }; + + mockResolveUsageWindowWithFallback.mockReturnValue({ + sessionDurationMs: 18000000, + elapsedMs: 3600000, + remainingMs: 14400000, + elapsedPercent: 20, + remainingPercent: 80 + }); + mockFormatUsageDuration.mockReturnValue('4hr'); + + expect(render(widget, item, { usageData: { sessionUsage: 0 } })).toBeNull(); + // A null bucket pins usage to 0 while limits[] still supplies the reset time + expect(render(widget, item, { usageData: { sessionUsage: 0, sessionResetAt: '2030-07-01T00:00:00.000Z' } })).toBe('Reset: 4hr'); + expect(render(widget, item, { usageData: { sessionUsage: 5 } })).toBe('Reset: 4hr'); + expect(render(widget, { id: 'reset', type: 'reset-timer' }, { usageData: { sessionUsage: 0 } })).toBe('Reset: 4hr'); }); // One state covers both placeholders, since either means the same thing to diff --git a/src/widgets/__tests__/ExtraUsageUsed.test.ts b/src/widgets/__tests__/ExtraUsageUsed.test.ts index 70be2b521..71e19beaf 100644 --- a/src/widgets/__tests__/ExtraUsageUsed.test.ts +++ b/src/widgets/__tests__/ExtraUsageUsed.test.ts @@ -95,13 +95,22 @@ describe('ExtraUsageUsedWidget', () => { expect(widget.supportsNumberFormat()).toBe(true); }); - it('declares the disabled and no-data hideable states', () => { + it('declares the disabled, no-data and zero hideable states', () => { const widget = new ExtraUsageUsedWidget(); - expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data']); + expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data', 'zero']); expect(widget.getEditorDisplay({ id: 'extra', type: 'extra-usage-used' }).modifierText).toBeUndefined(); }); + it('hides zero spend only when the zero state is enabled', () => { + const widget = new ExtraUsageUsedWidget(); + const hideZeroItem: WidgetItem = { id: 'extra', type: 'extra-usage-used', metadata: { hide: 'zero' } }; + + expect(render(widget, hideZeroItem, { usageData: { extraUsageEnabled: true, extraUsageUsed: 0 } })).toBeNull(); + expect(render(widget, hideZeroItem, { usageData: { extraUsageEnabled: true, extraUsageUsed: 10600 } })).toBe('Overage Used: $106.00'); + expect(render(widget, { id: 'extra', type: 'extra-usage-used' }, { usageData: { extraUsageEnabled: true, extraUsageUsed: 0 } })).toBe('Overage Used: $0.00'); + }); + it('renders available used budget before unrelated usage errors', () => { const widget = new ExtraUsageUsedWidget(); diff --git a/src/widgets/__tests__/ExtraUsageUtilization.test.ts b/src/widgets/__tests__/ExtraUsageUtilization.test.ts index 3098d7dff..3ae14f91f 100644 --- a/src/widgets/__tests__/ExtraUsageUtilization.test.ts +++ b/src/widgets/__tests__/ExtraUsageUtilization.test.ts @@ -150,7 +150,22 @@ describe('ExtraUsageUtilizationWidget', () => { metadata: { invert: 'true' } }).modifierText).toBe('(remaining)'); - expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data']); + expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data', 'zero']); + }); + + it('hides zero utilization only when the zero state is enabled, regardless of invert', () => { + const widget = new ExtraUsageUtilizationWidget(); + const zeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUtilization: 0 } }; + + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, zeroContext)).toBeNull(); + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero', invert: 'true' } }, zeroContext)).toBeNull(); + const nonZeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUtilization: 25 } }; + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, nonZeroContext)).toBe('Overage: 25.0%'); + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization' }, zeroContext)).toBe('Overage: 0.0%'); + + const derivedZeroContext: RenderContext = { usageData: { extraUsageEnabled: true, extraUsageUsed: 0, extraUsageLimit: 5000 } }; + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization', metadata: { hide: 'zero' } }, derivedZeroContext)).toBeNull(); + expect(render(widget, { id: 'extra', type: 'extra-usage-utilization' }, derivedZeroContext)).toBe('Overage: 0.0%'); }); it('does not show a time cursor carried over from a usage widget', () => { diff --git a/src/widgets/__tests__/WeeklyResetTimer.test.ts b/src/widgets/__tests__/WeeklyResetTimer.test.ts index efa575522..65be7736a 100644 --- a/src/widgets/__tests__/WeeklyResetTimer.test.ts +++ b/src/widgets/__tests__/WeeklyResetTimer.test.ts @@ -151,8 +151,28 @@ describe('WeeklyResetTimerWidget', () => { expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', metadata: { display: 'progress' } }, { usageData: {} })).toBe('Weekly Reset [Loading]'); }); - it('declares the no-data hideable state', () => { - expect(new WeeklyResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data']); + it('declares the no-data and zero hideable states', () => { + expect(new WeeklyResetTimerWidget().getHideableStates().map(state => state.key)).toEqual(['no-data', 'zero']); + }); + + it('hides when weeklyUsage is zero and the zero state is enabled', () => { + const widget = new WeeklyResetTimerWidget(); + const item: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer', metadata: { hide: 'zero' } }; + + mockResolveWeeklyUsageWindow.mockReturnValue({ + sessionDurationMs: 18000000, + elapsedMs: 3600000, + remainingMs: 14400000, + elapsedPercent: 20, + remainingPercent: 80 + }); + mockFormatUsageDuration.mockReturnValue('4hr'); + + expect(render(widget, item, { usageData: { weeklyUsage: 0 } })).toBeNull(); + // A null bucket pins usage to 0 while limits[] still supplies the reset time + expect(render(widget, item, { usageData: { weeklyUsage: 0, weeklyResetAt: '2030-07-01T00:00:00.000Z' } })).toBe('Weekly Reset: 4hr'); + expect(render(widget, item, { usageData: { weeklyUsage: 5 } })).toBe('Weekly Reset: 4hr'); + expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { usageData: { weeklyUsage: 0 } })).toBe('Weekly Reset: 4hr'); }); // One state covers both placeholders, since either means the same thing to diff --git a/src/widgets/__tests__/helpers/usage-widget-suites.ts b/src/widgets/__tests__/helpers/usage-widget-suites.ts index 9ec6790a2..e9fbd39d9 100644 --- a/src/widgets/__tests__/helpers/usage-widget-suites.ts +++ b/src/widgets/__tests__/helpers/usage-widget-suites.ts @@ -8,6 +8,7 @@ import { import type { RenderContext } from '../../../types/RenderContext'; import type { CustomKeybind, + HideableState, WidgetEditorDisplay, WidgetItem } from '../../../types/Widget'; @@ -15,6 +16,7 @@ import type { interface UsageWidgetLike { getCustomKeybinds(item?: WidgetItem): CustomKeybind[]; getEditorDisplay(item: WidgetItem): WidgetEditorDisplay; + getHideableStates(): HideableState[]; handleEditorAction(action: string, item: WidgetItem): WidgetItem | null; supportsRawValue(): boolean; } @@ -149,6 +151,33 @@ export function runUsagePercentWidgetSuite(conf }, { usageData: { error: 'timeout' } })).toBeNull(); }); + it('declares the no-data and zero hideable states', () => { + expect(config.createWidget().getHideableStates().map(state => state.key)).toEqual(['no-data', 'zero']); + }); + + it('hides zero usage when the zero state is enabled, regardless of invert', () => { + const widget = config.createWidget(); + const context = getUsageContext(config.usageField, 0); + + expect(config.render(widget, { ...config.baseItem, metadata: { hide: 'zero' } }, context)).toBeNull(); + expect(config.render(widget, { ...config.baseItem, metadata: { hide: 'zero', invert: 'true' } }, context)).toBeNull(); + }); + + it('renders zero usage when the zero state is off', () => { + const widget = config.createWidget(); + + const expectedZeroTime = config.expectedTime.replace(config.expectedRawTime, '0.0%'); + + expect(config.render(widget, config.baseItem, getUsageContext(config.usageField, 0))).toBe(expectedZeroTime); + }); + + it('renders non-zero usage when the zero state is enabled', () => { + const widget = config.createWidget(); + const context = getUsageContext(config.usageField, config.usageValue); + + expect(config.render(widget, { ...config.baseItem, metadata: { hide: 'zero' } }, context)).toBe(config.expectedTime); + }); + it('renders available usage data before unrelated usage errors', () => { const widget = config.createWidget(); const context: RenderContext = { diff --git a/src/widgets/shared/usage-display.ts b/src/widgets/shared/usage-display.ts index bafb249b3..fc463634f 100644 --- a/src/widgets/shared/usage-display.ts +++ b/src/widgets/shared/usage-display.ts @@ -20,6 +20,7 @@ export type UsageDisplayMode = 'time' | 'progress' | 'progress-short' | 'slider' // Shared by the usage percentage widgets and the reset timers, which render the // same error placeholders export const USAGE_NO_DATA_HIDEABLE_STATE: HideableState = { key: 'no-data', label: 'when usage data is unavailable' }; +export const USAGE_ZERO_HIDEABLE_STATE: HideableState = { key: 'zero', label: 'when usage is zero' }; const SLIDER_WIDTH = 10; diff --git a/src/widgets/shared/usage-percent-widget.ts b/src/widgets/shared/usage-percent-widget.ts index 9dcf88ca5..39aef7737 100644 --- a/src/widgets/shared/usage-percent-widget.ts +++ b/src/widgets/shared/usage-percent-widget.ts @@ -27,6 +27,7 @@ import { makeTimerProgressBar } from './progress-bar'; import { formatRawOrLabeledValue } from './raw-or-labeled'; import { USAGE_NO_DATA_HIDEABLE_STATE, + USAGE_ZERO_HIDEABLE_STATE, cycleUsageDisplayMode, getUsageDisplayMode, getUsageDisplayModifierText, @@ -198,6 +199,10 @@ export function renderUsagePercentWidgetValue( } const percent = Math.max(0, Math.min(100, usagePercent)); + if (percent === 0 && isHidden(item, USAGE_ZERO_HIDEABLE_STATE.key)) { + return null; + } + const renderedPercent = inverted ? 100 - percent : percent; return renderUsageDisplay(item, config.label, renderedPercent, format, () => {