Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/types/Widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export interface Widget {
* still take precedence (see custom-command's preserve-colors mode).
*/
preservesRenderedColors?(item: WidgetItem): boolean;

// Lets a widget override its foreground color from live data (e.g. budget
// severity). Returns a color to use instead of the configured one, or
// undefined to keep the static color. Resolved at the renderer's per-widget
// color sites; the global overrideForegroundColor still takes precedence.
getDynamicColor?(item: WidgetItem, context: RenderContext): string | undefined;
}

export interface WidgetEditorProps {
Expand Down
137 changes: 137 additions & 0 deletions src/utils/__tests__/renderer-dynamic-color.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
describe,
expect,
it
} from 'vitest';

import type { RenderContext } from '../../types/RenderContext';
import {
DEFAULT_SETTINGS,
type Settings
} from '../../types/Settings';
import type { WidgetItem } from '../../types/Widget';
import { renderStatusLine } from '../renderer';

interface PreRenderedWidget {
content: string;
plainLength: number;
widget: WidgetItem;
}

// Truecolor so every expected code is an unambiguous literal rather than a
// palette lookup.
const CONFIGURED = 'hex:0000FF';
const TIER_OK = 'hex:00FF00';
const TIER_WARN = 'hex:FFAA00';
const TIER_CRIT = 'hex:FF0000';

const ANSI_CONFIGURED = '\x1b[38;2;0;0;255m';
const ANSI_OK = '\x1b[38;2;0;255;0m';
const ANSI_WARN = '\x1b[38;2;255;170;0m';
const ANSI_CRIT = '\x1b[38;2;255;0;0m';

function createSettings(overrides: Partial<Settings> = {}): Settings {
return {
...DEFAULT_SETTINGS,
...overrides,
colorLevel: 3,
powerline: {
...DEFAULT_SETTINGS.powerline,
...(overrides.powerline ?? {})
}
};
}

function budgetItem(metadata: Record<string, string>): WidgetItem {
return {
id: 'usage',
type: 'extra-usage-used',
color: CONFIGURED,
metadata: {
budgetColorOk: TIER_OK,
budgetColorWarn: TIER_WARN,
budgetColorCrit: TIER_CRIT,
...metadata
}
};
}

function render(
item: WidgetItem,
utilization: number | undefined,
settingsOverrides: Partial<Settings> = {}
): string {
const settings = createSettings(settingsOverrides);
const context: RenderContext = {
isPreview: false,
terminalWidth: 200,
usageData: { extraUsageUtilization: utilization }
};
const preRendered: PreRenderedWidget[] = [{ content: '$106', plainLength: 4, widget: item }];
return renderStatusLine([item], settings, context, preRendered, []);
}

const powerline: Partial<Settings> = { powerline: { ...DEFAULT_SETTINGS.powerline, enabled: true } };

describe('renderer resolves a widget dynamic color', () => {
const cases = [
{
name: 'keeps the configured color when budget colors are off',
metadata: {},
utilization: 95,
expected: ANSI_CONFIGURED
},
{
name: 'keeps the configured color when utilization is unknown',
metadata: { budgetColors: 'true' },
utilization: undefined,
expected: ANSI_CONFIGURED
},
{
name: 'uses the ok tier below the warn threshold',
metadata: { budgetColors: 'true' },
utilization: 50,
expected: ANSI_OK
},
{
name: 'uses the warn tier at the warn threshold',
metadata: { budgetColors: 'true' },
utilization: 75,
expected: ANSI_WARN
},
{
name: 'uses the crit tier at the crit threshold',
metadata: { budgetColors: 'true' },
utilization: 90,
expected: ANSI_CRIT
}
] as const;

const paths: { path: string; settings: Partial<Settings> }[] = [
{ path: 'normal', settings: {} },
{ path: 'powerline', settings: powerline }
];

describe.each(paths)('$path path', ({ settings }) => {
it.each(cases)('$name', ({ metadata, utilization, expected }) => {
const out = render(budgetItem(metadata), utilization, settings);

expect(out).toContain(expected);
for (const other of [ANSI_CONFIGURED, ANSI_OK, ANSI_WARN, ANSI_CRIT]) {
if (other !== expected)
expect(out).not.toContain(other);
}
});

it('lets the global foreground override win over the dynamic color', () => {
const out = render(
budgetItem({ budgetColors: 'true' }),
95,
{ ...settings, overrideForegroundColor: 'hex:ABCDEF' }
);

expect(out).toContain('\x1b[38;2;171;205;239m');
expect(out).not.toContain(ANSI_CRIT);
});
});
});
6 changes: 3 additions & 3 deletions src/utils/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ function renderPowerlineStatusLine(
const paddedText = `${leadingPadding}${widgetText}${trailingPadding}`;

// Determine colors
let fgColor = widget.color ?? defaultColor;
let fgColor = widgetImpl?.getDynamicColor?.(widget, context) ?? widget.color ?? defaultColor;
let bgColor = widget.backgroundColor;

// Apply theme colors if a theme is set (and not 'custom')
Expand Down Expand Up @@ -1162,13 +1162,13 @@ export function renderStatusLine(
try {
let widgetText: string | undefined;
let defaultColor = 'white';
const widgetImpl = getWidget(widget.type);

// Use pre-rendered content
const preRendered = preRenderedWidgets[i];
if (preRendered?.content) {
widgetText = preRendered.content;
// Get default color from widget impl for consistency
const widgetImpl = getWidget(widget.type);
if (widgetImpl) {
defaultColor = widgetImpl.getDefaultColor();
}
Expand Down Expand Up @@ -1199,7 +1199,7 @@ export function renderStatusLine(
} else {
// Normal widget rendering with colors
elements.push({
content: applyColorsWithOverride(widgetText, widget.color ?? defaultColor, widget.backgroundColor, widget.bold, widget.dim),
content: applyColorsWithOverride(widgetText, widgetImpl?.getDynamicColor?.(widget, context) ?? widget.color ?? defaultColor, widget.backgroundColor, widget.bold, widget.dim),
type: widget.type,
widget
});
Expand Down
24 changes: 23 additions & 1 deletion src/widgets/ExtraUsageRemaining.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
HideableState,
Widget,
WidgetEditorDisplay,
Expand All @@ -9,6 +10,12 @@ import type {
import { resolveNumberFormat } from '../utils/number-format';
import { getUsageErrorMessage } from '../utils/usage';

import {
appendBudgetColorsModifier,
getBudgetColorsKeybind,
handleToggleBudgetColorsAction,
resolveBudgetColor
} from './shared/budget-color';
import { formatUsageCurrency } from './shared/currency';
import { EXTRA_USAGE_DISABLED_HIDEABLE_STATE } from './shared/extra-usage-disabled';
import { isHidden } from './shared/hideable';
Expand All @@ -22,13 +29,20 @@ export class ExtraUsageRemainingWidget implements Widget {
getCategory(): string { return 'Usage'; }

getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
return {
displayText: this.getDisplayName(),
modifierText: appendBudgetColorsModifier(undefined, item)
};
}

getHideableStates(): HideableState[] {
return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
return handleToggleBudgetColorsAction(action, item);
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const format = resolveNumberFormat('cost', item, settings);
if (context.isPreview) {
Expand Down Expand Up @@ -59,6 +73,14 @@ export class ExtraUsageRemainingWidget implements Widget {
return formatRawOrLabeledValue(item, 'Overage Left: ', formatted);
}

getCustomKeybinds(): CustomKeybind[] {
return [getBudgetColorsKeybind()];
}

getDynamicColor(item: WidgetItem, context: RenderContext): string | undefined {
return resolveBudgetColor(item, context.usageData?.extraUsageUtilization);
}

supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
supportsNumberFormat(): boolean { return true; }
Expand Down
24 changes: 23 additions & 1 deletion src/widgets/ExtraUsageUsed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { RenderContext } from '../types/RenderContext';
import type { Settings } from '../types/Settings';
import type {
CustomKeybind,
HideableState,
Widget,
WidgetEditorDisplay,
Expand All @@ -9,6 +10,12 @@ import type {
import { resolveNumberFormat } from '../utils/number-format';
import { getUsageErrorMessage } from '../utils/usage';

import {
appendBudgetColorsModifier,
getBudgetColorsKeybind,
handleToggleBudgetColorsAction,
resolveBudgetColor
} from './shared/budget-color';
import { formatUsageCurrency } from './shared/currency';
import { EXTRA_USAGE_DISABLED_HIDEABLE_STATE } from './shared/extra-usage-disabled';
import { isHidden } from './shared/hideable';
Expand All @@ -22,13 +29,20 @@ export class ExtraUsageUsedWidget implements Widget {
getCategory(): string { return 'Usage'; }

getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return { displayText: this.getDisplayName() };
return {
displayText: this.getDisplayName(),
modifierText: appendBudgetColorsModifier(undefined, item)
};
}

getHideableStates(): HideableState[] {
return [EXTRA_USAGE_DISABLED_HIDEABLE_STATE, USAGE_NO_DATA_HIDEABLE_STATE];
}

handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
return handleToggleBudgetColorsAction(action, item);
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
const format = resolveNumberFormat('cost', item, settings);
if (context.isPreview) {
Expand Down Expand Up @@ -57,6 +71,14 @@ export class ExtraUsageUsedWidget implements Widget {
return formatRawOrLabeledValue(item, 'Overage Used: ', formatted);
}

getCustomKeybinds(): CustomKeybind[] {
return [getBudgetColorsKeybind()];
}

getDynamicColor(item: WidgetItem, context: RenderContext): string | undefined {
return resolveBudgetColor(item, context.usageData?.extraUsageUtilization);
}

supportsRawValue(): boolean { return true; }
supportsColors(item: WidgetItem): boolean { return true; }
supportsNumberFormat(): boolean { return true; }
Expand Down
19 changes: 16 additions & 3 deletions src/widgets/ExtraUsageUtilization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ import {
} from '../utils/number-format';
import { getUsageErrorMessage } from '../utils/usage';

import {
appendBudgetColorsModifier,
getBudgetColorsKeybind,
handleToggleBudgetColorsAction,
resolveBudgetColor
} from './shared/budget-color';
import { EXTRA_USAGE_DISABLED_HIDEABLE_STATE } from './shared/extra-usage-disabled';
import { isHidden } from './shared/hideable';
import { makeTimerProgressBar } from './shared/progress-bar';
Expand Down Expand Up @@ -40,7 +46,10 @@ export class ExtraUsageUtilizationWidget implements Widget {
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
return {
displayText: this.getDisplayName(),
modifierText: getUsageDisplayModifierText(item, { showUsageDirection: true })
modifierText: appendBudgetColorsModifier(
getUsageDisplayModifierText(item, { showUsageDirection: true }),
item
)
};
}

Expand All @@ -57,7 +66,7 @@ export class ExtraUsageUtilizationWidget implements Widget {
return toggleUsageInverted(item);
}

return null;
return handleToggleBudgetColorsAction(action, item);
}

render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
Expand Down Expand Up @@ -119,7 +128,11 @@ export class ExtraUsageUtilizationWidget implements Widget {
}

getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
return getUsagePercentCustomKeybinds(item, false);
return [...getUsagePercentCustomKeybinds(item, false), getBudgetColorsKeybind()];
}

getDynamicColor(item: WidgetItem, context: RenderContext): string | undefined {
return resolveBudgetColor(item, context.usageData?.extraUsageUtilization);
}

supportsRawValue(): boolean { return true; }
Expand Down
3 changes: 3 additions & 0 deletions src/widgets/__tests__/ExtraUsageRemaining.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ describe('ExtraUsageRemainingWidget', () => {
const baseItem: WidgetItem = { id: 'extra', type: 'extra-usage-remaining' };

expect(widget.getHideableStates().map(state => state.key)).toEqual(['disabled', 'no-data']);
expect(widget.getCustomKeybinds()).toEqual([
{ key: 'b', label: '(b)udget colors', action: 'toggle-budget-colors' }
]);
expect(widget.getEditorDisplay(baseItem).modifierText).toBeUndefined();
});

Expand Down
4 changes: 4 additions & 0 deletions src/widgets/__tests__/ExtraUsageUsed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ describe('ExtraUsageUsedWidget', () => {
type: 'extra-usage-used'
}, { isPreview: true }, settings)).toBe('Overage Used: $106');
expect(widget.supportsNumberFormat()).toBe(true);
expect(widget.getCustomKeybinds()).toEqual([
{ key: 'b', label: '(b)udget colors', action: 'toggle-budget-colors' }
]);
expect(widget.getEditorDisplay({ id: 'extra', type: 'extra-usage-used' }).modifierText).toBeUndefined();
});

it('declares the disabled and no-data hideable states', () => {
Expand Down
9 changes: 6 additions & 3 deletions src/widgets/__tests__/ExtraUsageUtilization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,24 @@ describe('ExtraUsageUtilizationWidget', () => {

expect(widget.getCustomKeybinds(baseItem)).toEqual([
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'u', label: '(u) show remaining', action: 'toggle-invert' }
{ key: 'u', label: '(u) show remaining', action: 'toggle-invert' },
{ key: 'b', label: '(b)udget colors', action: 'toggle-budget-colors' }
]);
expect(widget.getCustomKeybinds({
...baseItem,
metadata: { display: 'progress' }
})).toEqual([
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'u', label: '(u) show remaining', action: 'toggle-invert' }
{ key: 'u', label: '(u) show remaining', action: 'toggle-invert' },
{ key: 'b', label: '(b)udget colors', action: 'toggle-budget-colors' }
]);
expect(widget.getCustomKeybinds({
...baseItem,
metadata: { invert: 'true' }
})).toEqual([
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
{ key: 'u', label: '(u) show used', action: 'toggle-invert' }
{ key: 'u', label: '(u) show used', action: 'toggle-invert' },
{ key: 'b', label: '(b)udget colors', action: 'toggle-budget-colors' }
]);
expect(widget.getEditorDisplay(baseItem).modifierText).toBe('(used)');
expect(widget.getEditorDisplay({
Expand Down
Loading
Loading