diff --git a/src/tui/components/ItemsEditor.tsx b/src/tui/components/ItemsEditor.tsx index c4d1458..d68fbdc 100644 --- a/src/tui/components/ItemsEditor.tsx +++ b/src/tui/components/ItemsEditor.tsx @@ -32,6 +32,7 @@ import { type WidgetPickerAction, type WidgetPickerState } from './items-editor/input-handlers'; +import { shouldShowCustomKeybind } from './items-editor/keybind-visibility'; export interface ItemsEditorProps { widgets: WidgetItem[]; @@ -94,18 +95,6 @@ export const ItemsEditor: React.FC = ({ widgets, onUpdate, onB setCustomEditorWidget(null); }; - const shouldShowCustomKeybind = (widget: WidgetItem, keybind: CustomKeybind): boolean => { - if (keybind.action !== 'toggle-invert') { - if (keybind.action === 'edit-list-limit') { - return widget.type === 'skills' && widget.metadata?.mode === 'list'; - } - return true; - } - - const mode = widget.metadata?.display; - return mode === 'progress' || mode === 'progress-short'; - }; - const getVisibleCustomKeybinds = (widgetImpl: Widget, widget: WidgetItem): CustomKeybind[] => { if (!widgetImpl.getCustomKeybinds) { return []; diff --git a/src/tui/components/items-editor/__tests__/keybind-visibility.test.ts b/src/tui/components/items-editor/__tests__/keybind-visibility.test.ts new file mode 100644 index 0000000..98919f1 --- /dev/null +++ b/src/tui/components/items-editor/__tests__/keybind-visibility.test.ts @@ -0,0 +1,44 @@ +import { + describe, + expect, + it +} from 'vitest'; + +import type { + CustomKeybind, + WidgetItem +} from '../../../../types/Widget'; +import { shouldShowCustomKeybind } from '../keybind-visibility'; + +const TOGGLE_COMPACT_KEYBIND: CustomKeybind = { key: 's', label: '(s)hort time', action: 'toggle-compact' }; +const TOGGLE_INVERT_KEYBIND: CustomKeybind = { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }; +const EDIT_LIST_LIMIT_KEYBIND: CustomKeybind = { key: 'l', label: '(l)imit list', action: 'edit-list-limit' }; + +function createWidget(type: string, metadata?: Record): WidgetItem { + return { + id: 'widget', + type, + metadata + }; +} + +describe('shouldShowCustomKeybind', () => { + it('shows invert only in progress modes', () => { + expect(shouldShowCustomKeybind(createWidget('block-timer'), TOGGLE_INVERT_KEYBIND)).toBe(false); + expect(shouldShowCustomKeybind(createWidget('block-timer', { display: 'progress' }), TOGGLE_INVERT_KEYBIND)).toBe(true); + expect(shouldShowCustomKeybind(createWidget('block-timer', { display: 'progress-short' }), TOGGLE_INVERT_KEYBIND)).toBe(true); + }); + + it('hides short time in progress modes', () => { + expect(shouldShowCustomKeybind(createWidget('block-timer'), TOGGLE_COMPACT_KEYBIND)).toBe(true); + expect(shouldShowCustomKeybind(createWidget('block-timer', { display: 'time' }), TOGGLE_COMPACT_KEYBIND)).toBe(true); + expect(shouldShowCustomKeybind(createWidget('block-timer', { display: 'progress' }), TOGGLE_COMPACT_KEYBIND)).toBe(false); + expect(shouldShowCustomKeybind(createWidget('block-timer', { display: 'progress-short' }), TOGGLE_COMPACT_KEYBIND)).toBe(false); + }); + + it('shows list limit only for skills list mode', () => { + expect(shouldShowCustomKeybind(createWidget('skills'), EDIT_LIST_LIMIT_KEYBIND)).toBe(false); + expect(shouldShowCustomKeybind(createWidget('skills', { mode: 'count' }), EDIT_LIST_LIMIT_KEYBIND)).toBe(false); + expect(shouldShowCustomKeybind(createWidget('skills', { mode: 'list' }), EDIT_LIST_LIMIT_KEYBIND)).toBe(true); + }); +}); \ No newline at end of file diff --git a/src/tui/components/items-editor/keybind-visibility.ts b/src/tui/components/items-editor/keybind-visibility.ts new file mode 100644 index 0000000..b130e61 --- /dev/null +++ b/src/tui/components/items-editor/keybind-visibility.ts @@ -0,0 +1,25 @@ +import type { + CustomKeybind, + WidgetItem +} from '../../../types/Widget'; + +function isProgressMode(widget: WidgetItem): boolean { + const mode = widget.metadata?.display; + return mode === 'progress' || mode === 'progress-short'; +} + +export function shouldShowCustomKeybind(widget: WidgetItem, keybind: CustomKeybind): boolean { + if (keybind.action === 'edit-list-limit') { + return widget.type === 'skills' && widget.metadata?.mode === 'list'; + } + + if (keybind.action === 'toggle-invert') { + return isProgressMode(widget); + } + + if (keybind.action === 'toggle-compact') { + return !isProgressMode(widget); + } + + return true; +} \ No newline at end of file diff --git a/src/utils/__tests__/usage.test.ts b/src/utils/__tests__/usage.test.ts index 7bc03b1..f96f42b 100644 --- a/src/utils/__tests__/usage.test.ts +++ b/src/utils/__tests__/usage.test.ts @@ -139,4 +139,11 @@ describe('usage window helpers', () => { expect(formatUsageDuration(3.5 * 60 * 60 * 1000)).toBe('3hr 30m'); expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000)).toBe('4hr 5m'); }); + + it('formats duration in compact style', () => { + expect(formatUsageDuration(0, true)).toBe('0h'); + expect(formatUsageDuration(3 * 60 * 60 * 1000, true)).toBe('3h'); + expect(formatUsageDuration(3.5 * 60 * 60 * 1000, true)).toBe('3h30m'); + expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000, true)).toBe('4h5m'); + }); }); \ No newline at end of file diff --git a/src/utils/usage-windows.ts b/src/utils/usage-windows.ts index e994f87..11a2522 100644 --- a/src/utils/usage-windows.ts +++ b/src/utils/usage-windows.ts @@ -89,11 +89,15 @@ export function resolveWeeklyUsageWindow(usageData: UsageData, nowMs = Date.now( return getWeeklyUsageWindowFromResetAt(usageData.weeklyResetAt, nowMs); } -export function formatUsageDuration(durationMs: number): string { +export function formatUsageDuration(durationMs: number, compact = false): string { const clampedMs = Math.max(0, durationMs); const elapsedHours = Math.floor(clampedMs / (1000 * 60 * 60)); const elapsedMinutes = Math.floor((clampedMs % (1000 * 60 * 60)) / (1000 * 60)); + if (compact) { + return elapsedMinutes === 0 ? `${elapsedHours}h` : `${elapsedHours}h${elapsedMinutes}m`; + } + if (elapsedMinutes === 0) { return `${elapsedHours}hr`; } diff --git a/src/widgets/BlockResetTimer.ts b/src/widgets/BlockResetTimer.ts index 8453696..4c23b92 100644 --- a/src/widgets/BlockResetTimer.ts +++ b/src/widgets/BlockResetTimer.ts @@ -18,8 +18,10 @@ import { getUsageDisplayMode, getUsageDisplayModifierText, getUsageProgressBarWidth, + isUsageCompact, isUsageInverted, isUsageProgressMode, + toggleUsageCompact, toggleUsageInverted } from './shared/usage-display'; @@ -39,7 +41,7 @@ export class BlockResetTimerWidget implements Widget { getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { return { displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item) + modifierText: getUsageDisplayModifierText(item, { includeCompact: true }) }; } @@ -52,12 +54,17 @@ export class BlockResetTimerWidget implements Widget { return toggleUsageInverted(item); } + if (action === 'toggle-compact') { + return toggleUsageCompact(item); + } + return null; } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { const displayMode = getUsageDisplayMode(item); const inverted = isUsageInverted(item); + const compact = isUsageCompact(item); if (context.isPreview) { const previewPercent = inverted ? 90.0 : 10.0; @@ -68,7 +75,7 @@ export class BlockResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`); } - return formatRawOrLabeledValue(item, 'Reset: ', '4hr 30m'); + return formatRawOrLabeledValue(item, 'Reset: ', compact ? '4h30m' : '4hr 30m'); } const usageData = context.usageData ?? {}; @@ -90,14 +97,15 @@ export class BlockResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${percentage}%`); } - const remainingTime = formatUsageDuration(window.remainingMs); + const remainingTime = formatUsageDuration(window.remainingMs, compact); return formatRawOrLabeledValue(item, '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: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }, + { key: 's', label: '(s)hort time', action: 'toggle-compact' } ]; } diff --git a/src/widgets/BlockTimer.ts b/src/widgets/BlockTimer.ts index 8387592..bfce5d5 100644 --- a/src/widgets/BlockTimer.ts +++ b/src/widgets/BlockTimer.ts @@ -17,8 +17,10 @@ import { getUsageDisplayMode, getUsageDisplayModifierText, getUsageProgressBarWidth, + isUsageCompact, isUsageInverted, isUsageProgressMode, + toggleUsageCompact, toggleUsageInverted } from './shared/usage-display'; @@ -38,7 +40,7 @@ export class BlockTimerWidget implements Widget { getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { return { displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item) + modifierText: getUsageDisplayModifierText(item, { includeCompact: true }) }; } @@ -51,12 +53,17 @@ export class BlockTimerWidget implements Widget { return toggleUsageInverted(item); } + if (action === 'toggle-compact') { + return toggleUsageCompact(item); + } + return null; } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { const displayMode = getUsageDisplayMode(item); const inverted = isUsageInverted(item); + const compact = isUsageCompact(item); if (context.isPreview) { const previewPercent = inverted ? 26.1 : 73.9; @@ -67,7 +74,7 @@ export class BlockTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${previewPercent.toFixed(1)}%`); } - return formatRawOrLabeledValue(item, 'Block: ', '3hr 45m'); + return formatRawOrLabeledValue(item, 'Block: ', compact ? '3h45m' : '3hr 45m'); } const usageData = context.usageData ?? {}; @@ -80,7 +87,7 @@ export class BlockTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Block ', `[${emptyBar}] 0.0%`); } - return formatRawOrLabeledValue(item, 'Block: ', '0hr 0m'); + return formatRawOrLabeledValue(item, 'Block: ', compact ? '0h' : '0hr 0m'); } if (isUsageProgressMode(displayMode)) { @@ -91,14 +98,15 @@ export class BlockTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${percentage}%`); } - const elapsedTime = formatUsageDuration(window.elapsedMs); + const elapsedTime = formatUsageDuration(window.elapsedMs, compact); return formatRawOrLabeledValue(item, 'Block: ', elapsedTime); } getCustomKeybinds(): CustomKeybind[] { return [ { key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }, - { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' } + { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }, + { key: 's', label: '(s)hort time', action: 'toggle-compact' } ]; } diff --git a/src/widgets/WeeklyResetTimer.ts b/src/widgets/WeeklyResetTimer.ts index 166db52..ead9af7 100644 --- a/src/widgets/WeeklyResetTimer.ts +++ b/src/widgets/WeeklyResetTimer.ts @@ -18,8 +18,10 @@ import { getUsageDisplayMode, getUsageDisplayModifierText, getUsageProgressBarWidth, + isUsageCompact, isUsageInverted, isUsageProgressMode, + toggleUsageCompact, toggleUsageInverted } from './shared/usage-display'; @@ -39,7 +41,7 @@ export class WeeklyResetTimerWidget implements Widget { getEditorDisplay(item: WidgetItem): WidgetEditorDisplay { return { displayText: this.getDisplayName(), - modifierText: getUsageDisplayModifierText(item) + modifierText: getUsageDisplayModifierText(item, { includeCompact: true }) }; } @@ -52,12 +54,17 @@ export class WeeklyResetTimerWidget implements Widget { return toggleUsageInverted(item); } + if (action === 'toggle-compact') { + return toggleUsageCompact(item); + } + return null; } render(item: WidgetItem, context: RenderContext, settings: Settings): string | null { const displayMode = getUsageDisplayMode(item); const inverted = isUsageInverted(item); + const compact = isUsageCompact(item); if (context.isPreview) { const previewPercent = inverted ? 90.0 : 10.0; @@ -68,7 +75,7 @@ export class WeeklyResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`); } - return formatRawOrLabeledValue(item, 'Weekly Reset: ', '36hr 30m'); + return formatRawOrLabeledValue(item, 'Weekly Reset: ', compact ? '36h30m' : '36hr 30m'); } const usageData = context.usageData ?? {}; @@ -90,14 +97,15 @@ export class WeeklyResetTimerWidget implements Widget { return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${percentage}%`); } - const remainingTime = formatUsageDuration(window.remainingMs); + const remainingTime = 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: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }, + { key: 's', label: '(s)hort time', action: 'toggle-compact' } ]; } diff --git a/src/widgets/__tests__/helpers/usage-widget-suites.ts b/src/widgets/__tests__/helpers/usage-widget-suites.ts index 45e9b54..0e9c9af 100644 --- a/src/widgets/__tests__/helpers/usage-widget-suites.ts +++ b/src/widgets/__tests__/helpers/usage-widget-suites.ts @@ -50,6 +50,12 @@ const EXPECTED_USAGE_KEYBINDS: CustomKeybind[] = [ { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' } ]; +const EXPECTED_TIMER_KEYBINDS: CustomKeybind[] = [ + { 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' } +]; + function getUsageContext(field: 'sessionUsage' | 'weeklyUsage', value: number): RenderContext { return field === 'sessionUsage' ? { usageData: { sessionUsage: value } } @@ -140,6 +146,23 @@ export function runUsagePercentWidgetSuite(conf expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined(); expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText); }); + + it('ignores stale compact metadata in editor modifiers', () => { + const widget = config.createWidget(); + const modifierItemWithCompact: WidgetItem = { + ...config.modifierItem, + metadata: { + ...(config.modifierItem.metadata ?? {}), + compact: 'true' + } + }; + + expect(widget.getEditorDisplay({ + ...config.baseItem, + metadata: { compact: 'true' } + }).modifierText).toBeUndefined(); + expect(widget.getEditorDisplay(modifierItemWithCompact).modifierText).toBe(config.expectedModifierText); + }); } export function runUsageTimerEditorSuite(config: UsageTimerEditorSuiteConfig): void { @@ -147,12 +170,12 @@ export function runUsageTimerEditorSuite { + it('supports raw value and exposes progress/invert/compact keybinds', () => { const widget = config.createWidget(); expect(widget.getDisplayName()).toBe(config.expectedDisplayName); expect(widget.supportsRawValue()).toBe(true); - expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS); + expect(widget.getCustomKeybinds()).toEqual(EXPECTED_TIMER_KEYBINDS); }); it('clears invert metadata when cycling back to time mode', () => { @@ -192,4 +215,15 @@ export function runUsageTimerEditorSuite { + const widget = config.createWidget(); + + const compact = widget.handleEditorAction('toggle-compact', config.baseItem); + const cleared = widget.handleEditorAction('toggle-compact', compact ?? config.baseItem); + + expect(compact?.metadata?.compact).toBe('true'); + expect(cleared?.metadata?.compact).toBe('false'); + expect(widget.getEditorDisplay({ ...config.baseItem, metadata: { compact: 'true' } }).modifierText).toBe('(compact)'); + }); } \ No newline at end of file diff --git a/src/widgets/shared/usage-display.ts b/src/widgets/shared/usage-display.ts index 5d4b2d1..1c97e06 100644 --- a/src/widgets/shared/usage-display.ts +++ b/src/widgets/shared/usage-display.ts @@ -28,7 +28,20 @@ export function isUsageInverted(item: WidgetItem): boolean { return isMetadataFlagEnabled(item, 'invert'); } -export function getUsageDisplayModifierText(item: WidgetItem): string | undefined { +export function isUsageCompact(item: WidgetItem): boolean { + return isMetadataFlagEnabled(item, 'compact'); +} + +export function toggleUsageCompact(item: WidgetItem): WidgetItem { + return toggleMetadataFlag(item, 'compact'); +} + +interface UsageDisplayModifierOptions { includeCompact?: boolean } + +export function getUsageDisplayModifierText( + item: WidgetItem, + options: UsageDisplayModifierOptions = {} +): string | undefined { const mode = getUsageDisplayMode(item); const modifiers: string[] = []; @@ -42,6 +55,10 @@ export function getUsageDisplayModifierText(item: WidgetItem): string | undefine modifiers.push('inverted'); } + if (options.includeCompact && isUsageCompact(item)) { + modifiers.push('compact'); + } + return makeModifierText(modifiers); }