From c0791c32135f8e0b49c68af91b5bc807a1af4ce1 Mon Sep 17 00:00:00 2001 From: Justin Bishop Date: Sat, 14 Mar 2026 16:16:22 -0700 Subject: [PATCH 1/3] feat: add days+hours display format to weekly reset timer Add a new "days+hours" toggle (keybind 'd') to the weekly reset timer widget that displays remaining time as "6d 12h" instead of "156hr 30m". This format is more readable for the weekly time scale. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/__tests__/usage.test.ts | 10 ++++ src/utils/usage-windows.ts | 16 +++++++ src/utils/usage.ts | 1 + src/widgets/WeeklyResetTimer.ts | 21 ++++++-- .../__tests__/WeeklyResetTimer.test.ts | 48 +++++++++++++++++++ .../__tests__/helpers/usage-widget-suites.ts | 3 +- src/widgets/shared/usage-display.ts | 14 +++++- 7 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/utils/__tests__/usage.test.ts b/src/utils/__tests__/usage.test.ts index f96f42bb..1b55c58c 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('0h'); + expect(formatUsageDurationDaysHours(12 * 60 * 60 * 1000)).toBe('12h'); + expect(formatUsageDurationDaysHours(24 * 60 * 60 * 1000)).toBe('1d'); + expect(formatUsageDurationDaysHours(36 * 60 * 60 * 1000)).toBe('1d 12h'); + expect(formatUsageDurationDaysHours(6.5 * 24 * 60 * 60 * 1000)).toBe('6d 12h'); + 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 2d469c09..8b6c4429 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}h`; + } + + if (hours === 0) { + return `${days}d`; + } + + return `${days}d ${hours}h`; +} + 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 b67247af..a27b6b34 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 ead9af7f..36f1547b 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 12h'); + } + 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: 'd', label: '(d)ays+hours', action: 'toggle-days-hours' } ]; } diff --git a/src/widgets/__tests__/WeeklyResetTimer.test.ts b/src/widgets/__tests__/WeeklyResetTimer.test.ts index 341095be..4bf73f22 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 12h'); + }); + 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 14h'); + }); + 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: 'd', label: '(d)ays+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 0e9c9af4..2ec5ea80 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 1c97e065..e05196b5 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); } From f2b85464db21941cc29e819d67a96396404464c2 Mon Sep 17 00:00:00 2001 From: Justin Bishop Date: Sat, 14 Mar 2026 16:20:13 -0700 Subject: [PATCH 2/3] fix: use 'y' keybind for days+hours toggle to avoid conflict with delete Co-Authored-By: Claude Opus 4.6 (1M context) --- src/widgets/WeeklyResetTimer.ts | 2 +- src/widgets/__tests__/WeeklyResetTimer.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/WeeklyResetTimer.ts b/src/widgets/WeeklyResetTimer.ts index 36f1547b..c5a02211 100644 --- a/src/widgets/WeeklyResetTimer.ts +++ b/src/widgets/WeeklyResetTimer.ts @@ -120,7 +120,7 @@ export class WeeklyResetTimerWidget implements Widget { { 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: 'd', label: '(d)ays+hours', action: 'toggle-days-hours' } + { 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 4bf73f22..ae0de2a5 100644 --- a/src/widgets/__tests__/WeeklyResetTimer.test.ts +++ b/src/widgets/__tests__/WeeklyResetTimer.test.ts @@ -161,7 +161,7 @@ describe('WeeklyResetTimerWidget', () => { { 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: 'd', label: '(d)ays+hours', action: 'toggle-days-hours' } + { key: 'y', label: 'da(y)s+hours', action: 'toggle-days-hours' } ], expectedModifierText: '(short bar, inverted)', modifierItem: { From c99bff5c4c2c85e4e5aac4524397655d8d52bd5d Mon Sep 17 00:00:00 2001 From: Justin Bishop Date: Sat, 14 Mar 2026 16:24:49 -0700 Subject: [PATCH 3/3] fix: use 'hr' suffix instead of 'h' in days+hours format for consistency Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/__tests__/usage.test.ts | 8 ++++---- src/utils/usage-windows.ts | 4 ++-- src/widgets/WeeklyResetTimer.ts | 2 +- src/widgets/__tests__/WeeklyResetTimer.test.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/utils/__tests__/usage.test.ts b/src/utils/__tests__/usage.test.ts index 1b55c58c..d8b5d9bb 100644 --- a/src/utils/__tests__/usage.test.ts +++ b/src/utils/__tests__/usage.test.ts @@ -149,11 +149,11 @@ describe('usage window helpers', () => { }); it('formats duration in days+hours style', () => { - expect(formatUsageDurationDaysHours(0)).toBe('0h'); - expect(formatUsageDurationDaysHours(12 * 60 * 60 * 1000)).toBe('12h'); + 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 12h'); - expect(formatUsageDurationDaysHours(6.5 * 24 * 60 * 60 * 1000)).toBe('6d 12h'); + 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 8b6c4429..fc3d7edf 100644 --- a/src/utils/usage-windows.ts +++ b/src/utils/usage-windows.ts @@ -111,14 +111,14 @@ export function formatUsageDurationDaysHours(durationMs: number): string { const hours = Math.floor((clampedMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); if (days === 0) { - return `${hours}h`; + return `${hours}hr`; } if (hours === 0) { return `${days}d`; } - return `${days}d ${hours}h`; + return `${days}d ${hours}hr`; } export function getUsageErrorMessage(error: UsageError): string { diff --git a/src/widgets/WeeklyResetTimer.ts b/src/widgets/WeeklyResetTimer.ts index c5a02211..bbf6e300 100644 --- a/src/widgets/WeeklyResetTimer.ts +++ b/src/widgets/WeeklyResetTimer.ts @@ -84,7 +84,7 @@ export class WeeklyResetTimerWidget implements Widget { } if (daysHours) { - return formatRawOrLabeledValue(item, 'Weekly Reset: ', '1d 12h'); + return formatRawOrLabeledValue(item, 'Weekly Reset: ', '1d 12hr'); } return formatRawOrLabeledValue(item, 'Weekly Reset: ', compact ? '36h30m' : '36hr 30m'); diff --git a/src/widgets/__tests__/WeeklyResetTimer.test.ts b/src/widgets/__tests__/WeeklyResetTimer.test.ts index ae0de2a5..549cc5ce 100644 --- a/src/widgets/__tests__/WeeklyResetTimer.test.ts +++ b/src/widgets/__tests__/WeeklyResetTimer.test.ts @@ -50,7 +50,7 @@ describe('WeeklyResetTimerWidget', () => { metadata: { daysHours: 'true' } }; - expect(render(widget, item, { isPreview: true })).toBe('Weekly Reset: 1d 12h'); + expect(render(widget, item, { isPreview: true })).toBe('Weekly Reset: 1d 12hr'); }); it('renders remaining time in time mode', () => { @@ -123,7 +123,7 @@ describe('WeeklyResetTimerWidget', () => { remainingPercent: 80.1587301587 }); - expect(render(widget, item, { usageData: {} })).toBe('Weekly Reset: 5d 14h'); + expect(render(widget, item, { usageData: {} })).toBe('Weekly Reset: 5d 14hr'); }); it('shows raw value without label in time mode', () => {