diff --git a/src/widgets/BlockResetTimer.ts b/src/widgets/BlockResetTimer.ts index 692b2694..346273c5 100644 --- a/src/widgets/BlockResetTimer.ts +++ b/src/widgets/BlockResetTimer.ts @@ -32,6 +32,7 @@ import { getUsageLocale, getUsageProgressBarWidth, getUsageTimerCustomKeybinds, + getSliderBarChars, getUsageTimezone, isUsage12HourClock, isUsageCompact, @@ -108,7 +109,7 @@ export class BlockResetTimerWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(previewPercent); + const slider = makeSliderBar(previewPercent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${previewPercent.toFixed(1)}%` : slider; @@ -150,7 +151,7 @@ export class BlockResetTimerWidget implements Widget { if (isUsageSliderMode(displayMode)) { const percent = inverted ? window.remainingPercent : window.elapsedPercent; - const slider = makeSliderBar(percent); + const slider = makeSliderBar(percent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${percent.toFixed(1)}%` : slider; diff --git a/src/widgets/BlockTimer.ts b/src/widgets/BlockTimer.ts index 41a0ef45..423d856f 100644 --- a/src/widgets/BlockTimer.ts +++ b/src/widgets/BlockTimer.ts @@ -16,6 +16,7 @@ import { cycleUsageDisplayMode, getUsageDisplayMode, getUsageDisplayModifierText, + getSliderBarChars, getUsageProgressBarWidth, getUsageTimerCustomKeybinds, isUsageCompact, @@ -78,7 +79,7 @@ export class BlockTimerWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(previewPercent); + const slider = makeSliderBar(previewPercent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${previewPercent.toFixed(1)}%` : slider; @@ -99,7 +100,7 @@ export class BlockTimerWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const emptySlider = makeSliderBar(0); + const emptySlider = makeSliderBar(0, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${emptySlider} 0.0%` : emptySlider; @@ -119,7 +120,7 @@ export class BlockTimerWidget implements Widget { if (isUsageSliderMode(displayMode)) { const percent = inverted ? window.remainingPercent : window.elapsedPercent; - const slider = makeSliderBar(percent); + const slider = makeSliderBar(percent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${percent.toFixed(1)}%` : slider; diff --git a/src/widgets/ContextBar.ts b/src/widgets/ContextBar.ts index 400bc54d..608308fc 100644 --- a/src/widgets/ContextBar.ts +++ b/src/widgets/ContextBar.ts @@ -14,7 +14,7 @@ import { import { formatTokens } from '../utils/renderer'; import { makeUsageProgressBar } from '../utils/usage'; -import { makeSliderBar } from './shared/usage-display'; +import { getSliderBarChars, makeSliderBar } from './shared/usage-display'; type DisplayMode = 'progress' | 'progress-short' | 'slider' | 'slider-only'; @@ -82,7 +82,7 @@ export class ContextBarWidget implements Widget { if (context.isPreview) { if (isBarSliderMode(displayMode)) { - const slider = makeSliderBar(25); + const slider = makeSliderBar(25, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} 50k/200k (25%)` : slider; return item.rawValue ? sliderDisplay : `Context: ${sliderDisplay}`; } @@ -115,7 +115,7 @@ export class ContextBarWidget implements Widget { const totalDisplay = formatTokens(total, 0); if (isBarSliderMode(displayMode)) { - const slider = makeSliderBar(clampedPercent); + const slider = makeSliderBar(clampedPercent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${usedDisplay}/${totalDisplay} (${Math.round(clampedPercent)}%)` : slider; return item.rawValue ? sliderDisplay : `Context: ${sliderDisplay}`; } diff --git a/src/widgets/ContextPercentageUsable.ts b/src/widgets/ContextPercentageUsable.ts index 9dd8c679..88ae38ac 100644 --- a/src/widgets/ContextPercentageUsable.ts +++ b/src/widgets/ContextPercentageUsable.ts @@ -25,6 +25,7 @@ import { renderContextSlider } from './shared/context-slider'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; +import { getSliderBarChars } from './shared/usage-display'; export class ContextPercentageUsableWidget implements Widget { getDefaultColor(): string { return 'green'; } @@ -57,8 +58,9 @@ export class ContextPercentageUsableWidget implements Widget { const contextWindowMetrics = getContextWindowMetrics(context.data); const contextConfig = getContextConfig(modelIdentifier, contextWindowMetrics.windowSize); + const chars = getSliderBarChars(item); const formatContextPercentage = (displayPercentage: number): string => { - const sliderResult = renderContextSlider(sliderMode, displayPercentage); + const sliderResult = renderContextSlider(sliderMode, displayPercentage, chars); return formatRawOrLabeledValue(item, label, sliderResult ?? `${displayPercentage.toFixed(1)}%`); }; diff --git a/src/widgets/ExtraUsageUtilization.ts b/src/widgets/ExtraUsageUtilization.ts index 71dc3780..b8171a97 100644 --- a/src/widgets/ExtraUsageUtilization.ts +++ b/src/widgets/ExtraUsageUtilization.ts @@ -18,6 +18,7 @@ import { makeTimerProgressBar } from './shared/progress-bar'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { cycleUsageDisplayMode, + getSliderBarChars, getUsageDisplayMode, getUsageDisplayModifierText, getUsagePercentCustomKeybinds, @@ -74,7 +75,7 @@ export class ExtraUsageUtilizationWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent); + const slider = makeSliderBar(renderedPercent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Overage: ', sliderDisplay); } @@ -105,7 +106,7 @@ export class ExtraUsageUtilizationWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent); + const slider = makeSliderBar(renderedPercent, getSliderBarChars(item)); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Overage: ', sliderDisplay); } diff --git a/src/widgets/SessionUsage.ts b/src/widgets/SessionUsage.ts index 0a554e6e..db1b122c 100644 --- a/src/widgets/SessionUsage.ts +++ b/src/widgets/SessionUsage.ts @@ -15,6 +15,7 @@ import { makeTimerProgressBar } from './shared/progress-bar'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { cycleUsageDisplayMode, + getSliderBarChars, getUsageDisplayMode, getUsageDisplayModifierText, getUsagePercentCustomKeybinds, @@ -74,7 +75,8 @@ export class SessionUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); + const barChars = getSliderBarChars(item); + const slider = makeSliderBar(renderedPercent, showCursor ? { ...barChars, cursorPercent: 50 } : barChars); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Session: ', sliderDisplay); } @@ -109,7 +111,7 @@ export class SessionUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); + const slider = makeSliderBar(renderedPercent, { ...getSliderBarChars(item), ...getCursorOptions() }); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Session: ', sliderDisplay); } diff --git a/src/widgets/WeeklyOpusUsage.ts b/src/widgets/WeeklyOpusUsage.ts index 0049e04c..a5da7c3f 100644 --- a/src/widgets/WeeklyOpusUsage.ts +++ b/src/widgets/WeeklyOpusUsage.ts @@ -76,7 +76,7 @@ export class WeeklyOpusUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); + const slider = makeSliderBar(renderedPercent, showCursor ? { cursorPercent: 50 } : undefined); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, LABEL, sliderDisplay); } @@ -111,7 +111,7 @@ export class WeeklyOpusUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); + const slider = makeSliderBar(renderedPercent, getCursorOptions()); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, LABEL, sliderDisplay); } diff --git a/src/widgets/WeeklySonnetUsage.ts b/src/widgets/WeeklySonnetUsage.ts index 56be3a3e..8939381e 100644 --- a/src/widgets/WeeklySonnetUsage.ts +++ b/src/widgets/WeeklySonnetUsage.ts @@ -15,6 +15,7 @@ import { makeTimerProgressBar } from './shared/progress-bar'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { cycleUsageDisplayMode, + getSliderBarChars, getUsageDisplayMode, getUsageDisplayModifierText, getUsagePercentCustomKeybinds, @@ -76,7 +77,8 @@ export class WeeklySonnetUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); + const barChars = getSliderBarChars(item); + const slider = makeSliderBar(renderedPercent, showCursor ? { ...barChars, cursorPercent: 50 } : barChars); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, LABEL, sliderDisplay); } @@ -111,7 +113,7 @@ export class WeeklySonnetUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); + const slider = makeSliderBar(renderedPercent, { ...getSliderBarChars(item), ...getCursorOptions() }); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, LABEL, sliderDisplay); } diff --git a/src/widgets/WeeklyUsage.ts b/src/widgets/WeeklyUsage.ts index a9fc928c..e7831a47 100644 --- a/src/widgets/WeeklyUsage.ts +++ b/src/widgets/WeeklyUsage.ts @@ -15,6 +15,7 @@ import { makeTimerProgressBar } from './shared/progress-bar'; import { formatRawOrLabeledValue } from './shared/raw-or-labeled'; import { cycleUsageDisplayMode, + getSliderBarChars, getUsageDisplayMode, getUsageDisplayModifierText, getUsagePercentCustomKeybinds, @@ -74,7 +75,8 @@ export class WeeklyUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, showCursor ? { cursorPercent: 50 } : undefined); + const barChars = getSliderBarChars(item); + const slider = makeSliderBar(renderedPercent, showCursor ? { ...barChars, cursorPercent: 50 } : barChars); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Weekly: ', sliderDisplay); } @@ -109,7 +111,7 @@ export class WeeklyUsageWidget implements Widget { } if (isUsageSliderMode(displayMode)) { - const slider = makeSliderBar(renderedPercent, undefined, getCursorOptions()); + const slider = makeSliderBar(renderedPercent, { ...getSliderBarChars(item), ...getCursorOptions() }); const sliderDisplay = displayMode === 'slider' ? `${slider} ${renderedPercent.toFixed(1)}%` : slider; return formatRawOrLabeledValue(item, 'Weekly: ', sliderDisplay); } diff --git a/src/widgets/shared/__tests__/usage-display.test.ts b/src/widgets/shared/__tests__/usage-display.test.ts index 3cd7fae3..65ad8636 100644 --- a/src/widgets/shared/__tests__/usage-display.test.ts +++ b/src/widgets/shared/__tests__/usage-display.test.ts @@ -7,6 +7,7 @@ import { import type { WidgetItem } from '../../../types/Widget'; import { cycleUsageDisplayMode, + getSliderBarChars, makeSliderBar } from '../usage-display'; @@ -32,16 +33,45 @@ describe('makeSliderBar', () => { }); it('accepts custom width', () => { - expect(makeSliderBar(50, 6)).toBe('▓▓▓░░░'); + expect(makeSliderBar(50, { width: 6 })).toBe('▓▓▓░░░'); }); it('renders a time cursor when cursorPercent is provided', () => { - expect(makeSliderBar(50, 10, { cursorPercent: 50 })).toBe('▓▓▓▓▓│░░░░'); + expect(makeSliderBar(50, { cursorPercent: 50 })).toBe('▓▓▓▓▓│░░░░'); }); it('clamps slider cursor percent', () => { - expect(makeSliderBar(50, 10, { cursorPercent: -10 })).toBe('│▓▓▓▓░░░░░'); - expect(makeSliderBar(50, 10, { cursorPercent: 150 })).toBe('▓▓▓▓▓░░░░│'); + expect(makeSliderBar(50, { cursorPercent: -10 })).toBe('│▓▓▓▓░░░░░'); + expect(makeSliderBar(50, { cursorPercent: 150 })).toBe('▓▓▓▓▓░░░░│'); + }); + + it('uses custom filledChar and emptyChar', () => { + expect(makeSliderBar(50, { filledChar: '▰', emptyChar: '▱' })).toBe('▰▰▰▰▰▱▱▱▱▱'); + }); + + it('uses default chars when filledChar/emptyChar are not provided', () => { + expect(makeSliderBar(50, {})).toBe('▓▓▓▓▓░░░░░'); + }); + + it('custom chars work with cursor', () => { + expect(makeSliderBar(50, { filledChar: '▰', emptyChar: '▱', cursorPercent: 50 })).toBe('▰▰▰▰▰│▱▱▱▱'); + }); +}); + +describe('getSliderBarChars', () => { + it('returns defaults when metadata is absent', () => { + const item: WidgetItem = { id: 'test', type: 'session-usage' }; + expect(getSliderBarChars(item)).toEqual({ filledChar: '▓', emptyChar: '░' }); + }); + + it('returns custom chars from metadata', () => { + const item: WidgetItem = { id: 'test', type: 'session-usage', metadata: { filledChar: '▰', emptyChar: '▱' } }; + expect(getSliderBarChars(item)).toEqual({ filledChar: '▰', emptyChar: '▱' }); + }); + + it('falls back to defaults for empty string metadata values', () => { + const item: WidgetItem = { id: 'test', type: 'session-usage', metadata: { filledChar: '', emptyChar: '' } }; + expect(getSliderBarChars(item)).toEqual({ filledChar: '▓', emptyChar: '░' }); }); }); diff --git a/src/widgets/shared/context-slider.ts b/src/widgets/shared/context-slider.ts index 9157b0e8..b3dcb036 100644 --- a/src/widgets/shared/context-slider.ts +++ b/src/widgets/shared/context-slider.ts @@ -43,11 +43,15 @@ export function cycleContextSliderMode(item: WidgetItem): WidgetItem { }; } -export function renderContextSlider(mode: ContextSliderMode, percent: number): string | null { +export function renderContextSlider( + mode: ContextSliderMode, + percent: number, + chars?: { filledChar: string; emptyChar: string } +): string | null { if (mode === 'none') { return null; } - const slider = makeSliderBar(percent); + const slider = makeSliderBar(percent, chars); if (mode === 'slider') { return `${slider} ${percent.toFixed(1)}%`; } diff --git a/src/widgets/shared/usage-display.ts b/src/widgets/shared/usage-display.ts index ae84d5f1..215caa06 100644 --- a/src/widgets/shared/usage-display.ts +++ b/src/widgets/shared/usage-display.ts @@ -44,23 +44,41 @@ export function isUsageSliderMode(mode: UsageDisplayMode): boolean { return mode === 'slider' || mode === 'slider-only'; } -interface SliderBarOptions { cursorPercent?: number } +interface SliderBarOptions { + width?: number; + cursorPercent?: number; + filledChar?: string; + emptyChar?: string; +} + +export function getSliderBarChars(item: WidgetItem): { filledChar: string; emptyChar: string } { + const filledChar = typeof item.metadata?.filledChar === 'string' && item.metadata.filledChar.length > 0 + ? item.metadata.filledChar + : '▓'; + const emptyChar = typeof item.metadata?.emptyChar === 'string' && item.metadata.emptyChar.length > 0 + ? item.metadata.emptyChar + : '░'; + return { filledChar, emptyChar }; +} -export function makeSliderBar(percent: number, width: number = SLIDER_WIDTH, options?: SliderBarOptions): string { +export function makeSliderBar(percent: number, options?: SliderBarOptions): string { + const width = options?.width ?? SLIDER_WIDTH; const clamped = Math.max(0, Math.min(100, percent)); const filled = Math.round((clamped / 100) * width); const cursorPos = options?.cursorPercent !== undefined ? Math.min(Math.floor((Math.max(0, Math.min(100, options.cursorPercent)) / 100) * width), width - 1) : -1; + const filledChar = options?.filledChar ?? '▓'; + const emptyChar = options?.emptyChar ?? '░'; let bar = ''; for (let i = 0; i < width; i++) { if (i === cursorPos) { bar += '│'; } else if (i < filled) { - bar += '▓'; + bar += filledChar; } else { - bar += '░'; + bar += emptyChar; } }