Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/utils/__tests__/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '../usage-types';
import {
formatUsageDuration,
formatUsageDurationDaysHours,
getUsageWindowFromResetAt,
getWeeklyUsageWindowFromResetAt,
resolveUsageWindowWithFallback,
Expand Down Expand Up @@ -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');
});
});
16 changes: 16 additions & 0 deletions src/utils/usage-windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]';
Expand Down
1 change: 1 addition & 0 deletions src/utils/usage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { fetchUsageData } from './usage-fetch';
export {
formatUsageDuration,
formatUsageDurationDaysHours,
getUsageErrorMessage,
getUsageWindowFromBlockMetrics,
getUsageWindowFromResetAt,
Expand Down
21 changes: 18 additions & 3 deletions src/widgets/WeeklyResetTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from '../types/Widget';
import {
formatUsageDuration,
formatUsageDurationDaysHours,
getUsageErrorMessage,
resolveWeeklyUsageWindow
} from '../utils/usage';
Expand All @@ -19,9 +20,11 @@ import {
getUsageDisplayModifierText,
getUsageProgressBarWidth,
isUsageCompact,
isUsageDaysHours,
isUsageInverted,
isUsageProgressMode,
toggleUsageCompact,
toggleUsageDaysHours,
toggleUsageInverted
} from './shared/usage-display';

Expand All @@ -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 })
};
}

Expand All @@ -58,13 +61,18 @@ export class WeeklyResetTimerWidget implements Widget {
return toggleUsageCompact(item);
}

if (action === 'toggle-days-hours') {
return toggleUsageDaysHours(item);
}

return null;
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
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;
Expand All @@ -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');
}

Expand All @@ -97,15 +109,18 @@ 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);
}

getCustomKeybinds(): CustomKeybind[] {
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' }
];
}

Expand Down
48 changes: 48 additions & 0 deletions src/widgets/__tests__/WeeklyResetTimer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand All @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion src/widgets/__tests__/helpers/usage-widget-suites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface UsageTimerEditorSuiteConfig<TWidget extends UsageWidgetLike & { getDis
baseItem: WidgetItem;
createWidget: () => TWidget;
expectedDisplayName: string;
expectedKeybinds?: CustomKeybind[];
expectedModifierText: string;
modifierItem: WidgetItem;
}
Expand Down Expand Up @@ -175,7 +176,7 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get

expect(widget.getDisplayName()).toBe(config.expectedDisplayName);
expect(widget.supportsRawValue()).toBe(true);
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_TIMER_KEYBINDS);
expect(widget.getCustomKeybinds()).toEqual(config.expectedKeybinds ?? EXPECTED_TIMER_KEYBINDS);
});

it('clears invert metadata when cycling back to time mode', () => {
Expand Down
14 changes: 13 additions & 1 deletion src/widgets/shared/usage-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,6 +67,10 @@ export function getUsageDisplayModifierText(
modifiers.push('compact');
}

if (options.includeDaysHours && isUsageDaysHours(item)) {
modifiers.push('days+hours');
}

return makeModifierText(modifiers);
}

Expand Down