diff --git a/src/utils/__tests__/usage.test.ts b/src/utils/__tests__/usage.test.ts index f96f42b..d8b5d9b 100644 --- a/src/utils/__tests__/usage.test.ts +++ b/src/utils/__tests__/usage.test.ts @@ -15,6 +15,7 @@ import { } from '../usage-types'; import { formatUsageDuration, + formatUsageDurationDaysHours, getUsageWindowFromResetAt, getWeeklyUsageWindowFromResetAt, resolveUsageWindowWithFallback, @@ -146,4 +147,13 @@ describe('usage window helpers', () => { expect(formatUsageDuration(3.5 * 60 * 60 * 1000, true)).toBe('3h30m'); expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000, true)).toBe('4h5m'); }); + + it('formats duration in days+hours style', () => { + expect(formatUsageDurationDaysHours(0)).toBe('0hr'); + expect(formatUsageDurationDaysHours(12 * 60 * 60 * 1000)).toBe('12hr'); + expect(formatUsageDurationDaysHours(24 * 60 * 60 * 1000)).toBe('1d'); + expect(formatUsageDurationDaysHours(36 * 60 * 60 * 1000)).toBe('1d 12hr'); + expect(formatUsageDurationDaysHours(6.5 * 24 * 60 * 60 * 1000)).toBe('6d 12hr'); + expect(formatUsageDurationDaysHours(7 * 24 * 60 * 60 * 1000)).toBe('7d'); + }); }); \ No newline at end of file diff --git a/src/utils/usage-windows.ts b/src/utils/usage-windows.ts index 2d469c0..fc3d7ed 100644 --- a/src/utils/usage-windows.ts +++ b/src/utils/usage-windows.ts @@ -105,6 +105,22 @@ export function formatUsageDuration(durationMs: number, compact = false): string return `${elapsedHours}hr ${elapsedMinutes}m`; } +export function formatUsageDurationDaysHours(durationMs: number): string { + const clampedMs = Math.max(0, durationMs); + const days = Math.floor(clampedMs / (1000 * 60 * 60 * 24)); + const hours = Math.floor((clampedMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + + if (days === 0) { + return `${hours}hr`; + } + + if (hours === 0) { + return `${days}d`; + } + + return `${days}d ${hours}hr`; +} + export function getUsageErrorMessage(error: UsageError): string { switch (error) { case 'no-credentials': return '[No credentials]'; diff --git a/src/utils/usage.ts b/src/utils/usage.ts index b67247a..a27b6b3 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -1,6 +1,7 @@ export { fetchUsageData } from './usage-fetch'; export { formatUsageDuration, + formatUsageDurationDaysHours, getUsageErrorMessage, getUsageWindowFromBlockMetrics, getUsageWindowFromResetAt, diff --git a/src/widgets/WeeklyResetTimer.ts b/src/widgets/WeeklyResetTimer.ts index ead9af7..bbf6e30 100644 --- a/src/widgets/WeeklyResetTimer.ts +++ b/src/widgets/WeeklyResetTimer.ts @@ -8,6 +8,7 @@ import type { } from '../types/Widget'; import { formatUsageDuration, + formatUsageDurationDaysHours, getUsageErrorMessage, resolveWeeklyUsageWindow } from '../utils/usage'; @@ -19,9 +20,11 @@ import { getUsageDisplayModifierText, getUsageProgressBarWidth, isUsageCompact, + isUsageDaysHours, isUsageInverted, isUsageProgressMode, toggleUsageCompact, + toggleUsageDaysHours, toggleUsageInverted } from './shared/usage-display'; @@ -41,7 +44,7 @@ export class WeeklyResetTimerWidget implements Widget { getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { return { displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item, { includeCompact: true }) + modifierText: getUsageDisplayModifierText(item, { includeCompact: true, includeDaysHours: true }) }; } @@ -58,6 +61,10 @@ export class WeeklyResetTimerWidget implements Widget { return toggleUsageCompact(item); } + if (action === 'toggle-days-hours') { + return toggleUsageDaysHours(item); + } + return null; } @@ -65,6 +72,7 @@ export class WeeklyResetTimerWidget implements Widget { const displayMode = getUsageDisplayMode(item); const inverted = isUsageInverted(item); const compact = isUsageCompact(item); + const daysHours = isUsageDaysHours(item); if (context.isPreview) { const previewPercent = inverted ? 90.0 : 10.0; @@ -75,6 +83,10 @@ export class WeeklyResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`); } + if (daysHours) { + return formatRawOrLabeledValue(item, 'Weekly Reset: ', '1d 12hr'); + } + return formatRawOrLabeledValue(item, 'Weekly Reset: ', compact ? '36h30m' : '36hr 30m'); } @@ -97,7 +109,9 @@ export class WeeklyResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${percentage}%`); } - const remainingTime = formatUsageDuration(window.remainingMs, compact); + const remainingTime = daysHours + ? formatUsageDurationDaysHours(window.remainingMs) + : formatUsageDuration(window.remainingMs, compact); return formatRawOrLabeledValue(item, 'Weekly Reset: ', remainingTime); } @@ -105,7 +119,8 @@ export class WeeklyResetTimerWidget implements Widget { return [ { key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }, { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }, - { key: 's', label: '(s)hort time', action: 'toggle-compact' } + { key: 's', label: '(s)hort time', action: 'toggle-compact' }, + { key: 'y', label: 'da(y)s+hours', action: 'toggle-days-hours' } ]; } diff --git a/src/widgets/__tests__/WeeklyResetTimer.test.ts b/src/widgets/__tests__/WeeklyResetTimer.test.ts index 341095b..549cc5c 100644 --- a/src/widgets/__tests__/WeeklyResetTimer.test.ts +++ b/src/widgets/__tests__/WeeklyResetTimer.test.ts @@ -42,6 +42,17 @@ describe('WeeklyResetTimerWidget', () => { expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { isPreview: true })).toBe('Weekly Reset: 36hr 30m'); }); + it('renders preview using days+hours format', () => { + const widget = new WeeklyResetTimerWidget(); + const item: WidgetItem = { + id: 'weekly-reset', + type: 'weekly-reset-timer', + metadata: { daysHours: 'true' } + }; + + expect(render(widget, item, { isPreview: true })).toBe('Weekly Reset: 1d 12hr'); + }); + it('renders remaining time in time mode', () => { const widget = new WeeklyResetTimerWidget(); @@ -96,6 +107,25 @@ describe('WeeklyResetTimerWidget', () => { expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { usageData: {} })).toBeNull(); }); + it('renders remaining time in days+hours format', () => { + const widget = new WeeklyResetTimerWidget(); + const item: WidgetItem = { + id: 'weekly-reset', + type: 'weekly-reset-timer', + metadata: { daysHours: 'true' } + }; + + mockResolveWeeklyUsageWindow.mockReturnValue({ + sessionDurationMs: 604800000, + elapsedMs: 120000000, + remainingMs: 484800000, + elapsedPercent: 19.8412698413, + remainingPercent: 80.1587301587 + }); + + expect(render(widget, item, { usageData: {} })).toBe('Weekly Reset: 5d 14hr'); + }); + it('shows raw value without label in time mode', () => { const widget = new WeeklyResetTimerWidget(); @@ -111,10 +141,28 @@ describe('WeeklyResetTimerWidget', () => { expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', rawValue: true }, { usageData: {} })).toBe('120hr 15m'); }); + it('toggles days+hours metadata and shows modifier text', () => { + const widget = new WeeklyResetTimerWidget(); + const baseItem: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer' }; + + const toggled = widget.handleEditorAction('toggle-days-hours', baseItem); + const cleared = widget.handleEditorAction('toggle-days-hours', toggled ?? baseItem); + + expect(toggled?.metadata?.daysHours).toBe('true'); + expect(cleared?.metadata?.daysHours).toBe('false'); + expect(widget.getEditorDisplay({ ...baseItem, metadata: { daysHours: 'true' } }).modifierText).toBe('(days+hours)'); + }); + runUsageTimerEditorSuite({ baseItem: { id: 'weekly-reset', type: 'weekly-reset-timer' }, createWidget: () => new WeeklyResetTimerWidget(), expectedDisplayName: 'Weekly Reset Timer', + expectedKeybinds: [ + { key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }, + { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }, + { key: 's', label: '(s)hort time', action: 'toggle-compact' }, + { key: 'y', label: 'da(y)s+hours', action: 'toggle-days-hours' } + ], expectedModifierText: '(short bar, inverted)', modifierItem: { id: 'weekly-reset', diff --git a/src/widgets/__tests__/helpers/usage-widget-suites.ts b/src/widgets/__tests__/helpers/usage-widget-suites.ts index 0e9c9af..2ec5ea8 100644 --- a/src/widgets/__tests__/helpers/usage-widget-suites.ts +++ b/src/widgets/__tests__/helpers/usage-widget-suites.ts @@ -41,6 +41,7 @@ interface UsageTimerEditorSuiteConfig TWidget; expectedDisplayName: string; + expectedKeybinds?: CustomKeybind[]; expectedModifierText: string; modifierItem: WidgetItem; } @@ -175,7 +176,7 @@ export function runUsageTimerEditorSuite { diff --git a/src/widgets/shared/usage-display.ts b/src/widgets/shared/usage-display.ts index 1c97e06..e05196b 100644 --- a/src/widgets/shared/usage-display.ts +++ b/src/widgets/shared/usage-display.ts @@ -36,7 +36,15 @@ export function toggleUsageCompact(item: WidgetItem): WidgetItem { return toggleMetadataFlag(item, 'compact'); } -interface UsageDisplayModifierOptions { includeCompact?: boolean } +export function isUsageDaysHours(item: WidgetItem): boolean { + return isMetadataFlagEnabled(item, 'daysHours'); +} + +export function toggleUsageDaysHours(item: WidgetItem): WidgetItem { + return toggleMetadataFlag(item, 'daysHours'); +} + +interface UsageDisplayModifierOptions { includeCompact?: boolean; includeDaysHours?: boolean } export function getUsageDisplayModifierText( item: WidgetItem, @@ -59,6 +67,10 @@ export function getUsageDisplayModifierText( modifiers.push('compact'); } + if (options.includeDaysHours && isUsageDaysHours(item)) { + modifiers.push('days+hours'); + } + return makeModifierText(modifiers); }