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
16 changes: 14 additions & 2 deletions src/tui/components/ColorMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
clearAllWidgetStyling,
cycleWidgetColor,
cycleWidgetDim,
cycleWidgetNumberStyle,
resetWidgetStyling,
setWidgetColor,
toggleWidgetBold
Expand Down Expand Up @@ -278,6 +279,15 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
onUpdate(newItems);
}
}
} else if (input === 'n' || input === 'N') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Cycle number format style for the highlighted item: default -> compact -> whole -> default
const selectedWidget = colorableWidgets.find(widget => widget.id === highlightedItemId);
if (selectedWidget) {
const newItems = cycleWidgetNumberStyle(widgets, selectedWidget.id);
onUpdate(newItems);
}
}
} else if (input === 'r' || input === 'R') {
if (highlightedItemId && highlightedItemId !== 'back') {
// Reset all styling (color, background, and bold) for the highlighted item
Expand Down Expand Up @@ -444,7 +454,9 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
const styleIndicators = [
selectedWidget?.bold ? '[BOLD]' : null,
selectedWidget?.dim === true ? '[DIM]' : null,
selectedWidget?.dim === 'parens' ? '[DIM ()]' : null
selectedWidget?.dim === 'parens' ? '[DIM ()]' : null,
selectedWidget?.numberFormat?.style ? `[#${selectedWidget.numberFormat.style}]` : null,
selectedWidget?.numberFormat?.decimals !== undefined ? `[#${selectedWidget.numberFormat.decimals}dp]` : null
].filter(indicator => indicator !== null).join(' ');

// Gradient selection mode takes over the whole view
Expand Down Expand Up @@ -588,7 +600,7 @@ export const ColorMenu: React.FC<ColorMenuProps> = ({ widgets, lineIndex, settin
↑↓ to select, ←→ to cycle
{' '}
{editingBackground ? 'background' : 'foreground'}
, (f) to toggle bg/fg, (b)old, (d)im,
, (f) to toggle bg/fg, (b)old, (d)im, (n)umber,
{settings.colorLevel === 3 ? ' (h)ex,' : settings.colorLevel === 2 ? ' (a)nsi256,' : ''}
{!editingBackground && settings.colorLevel >= 2 ? ' (g)radient,' : ''}
{' '}
Expand Down
87 changes: 87 additions & 0 deletions src/tui/components/GlobalOverridesMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import {
import React, { useState } from 'react';

import { getColorLevelString } from '../../types/ColorLevel';
import {
NUMBER_KINDS,
type GlobalNumberFormat,
type NumberFormat,
type NumberKind,
type NumberStyle
} from '../../types/NumberFormat';
import type { Settings } from '../../types/Settings';
import {
COLOR_MAP,
Expand All @@ -18,6 +25,34 @@ import { shouldInsertInput } from '../../utils/input-guards';

import { ConfirmDialog } from './ConfirmDialog';

const NUMBER_FORMAT_STYLES: (NumberStyle | undefined)[] = [undefined, 'compact', 'whole'];

// Cycle a number kind's global style: default (precise) -> compact -> whole -> default.
// A global style forces that kind across all widgets (see resolveNumberFormat).
function cycleGlobalNumberStyle(settings: Settings, kind: NumberKind): Settings {
const current = settings.numberFormat?.[kind]?.style;
const currentIndex = NUMBER_FORMAT_STYLES.indexOf(current);
const nextStyle = NUMBER_FORMAT_STYLES[(currentIndex + 1) % NUMBER_FORMAT_STYLES.length];

const kindFormat: NumberFormat = { ...settings.numberFormat?.[kind] };
if (nextStyle === undefined) {
delete kindFormat.style;
} else {
kindFormat.style = nextStyle;
}

const { [kind]: removedKind, ...restGlobal } = settings.numberFormat ?? {};
void removedKind; // Intentionally unused
const nextGlobal: GlobalNumberFormat = Object.keys(kindFormat).length > 0
? { ...restGlobal, [kind]: kindFormat }
: restGlobal;

return {
...settings,
numberFormat: Object.keys(nextGlobal).length > 0 ? nextGlobal : undefined
};
}

export interface GlobalOverridesMenuProps {
settings: Settings;
onUpdate: (settings: Settings) => void;
Expand All @@ -33,6 +68,8 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
const [inheritColors, setInheritColors] = useState(settings.inheritSeparatorColors);
const [globalBold, setGlobalBold] = useState(settings.globalBold);
const [minimalistMode, setMinimalistMode] = useState(settings.minimalistMode);
const [numberFormatMode, setNumberFormatMode] = useState(false);
const [numberFormatKindIndex, setNumberFormatKindIndex] = useState(0);
const [gradientMode, setGradientMode] = useState(false);
const [gradientIndex, setGradientIndex] = useState(0);
const [gradientCustomStep, setGradientCustomStep] = useState<'start' | 'end' | null>(null);
Expand Down Expand Up @@ -159,6 +196,19 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
setGradientCustomStep('start');
}
}
} else if (numberFormatMode) {
if (key.escape) {
setNumberFormatMode(false);
} else if (key.upArrow) {
setNumberFormatKindIndex((numberFormatKindIndex - 1 + NUMBER_KINDS.length) % NUMBER_KINDS.length);
} else if (key.downArrow) {
setNumberFormatKindIndex((numberFormatKindIndex + 1) % NUMBER_KINDS.length);
} else if (key.leftArrow || key.rightArrow) {
const kind = NUMBER_KINDS[numberFormatKindIndex];
if (kind) {
onUpdate(cycleGlobalNumberStyle(settings, kind));
}
}
} else {
if (key.escape) {
onBack();
Expand Down Expand Up @@ -208,6 +258,9 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
minimalistMode: newMinimalistMode
};
onUpdate(updatedSettings);
} else if (input === 'n' || input === 'N') {
setNumberFormatMode(true);
setNumberFormatKindIndex(0);
} else if (input === 'f' || input === 'F') {
// Cycle through foreground colors
const nextIndex = (currentFgIndex + 1) % fgColors.length;
Expand Down Expand Up @@ -235,6 +288,34 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
}
});

if (numberFormatMode) {
return (
<Box flexDirection='column'>
<Text bold>Global Number Formatting</Text>
<Box marginTop={1}>
<Text dimColor>↑↓ to select a number type, ←→ to cycle its style, ESC to go back</Text>
</Box>
<Box marginTop={1} flexDirection='column'>
{NUMBER_KINDS.map((kind, idx) => {
const style = settings.numberFormat?.[kind]?.style ?? 'precise (default)';
return (
<Text key={kind} color={idx === numberFormatKindIndex ? 'cyan' : undefined}>
{idx === numberFormatKindIndex ? '▶ ' : ' '}
{kind}
{': '}
{style}
</Text>
);
})}
</Box>
<Box marginTop={1} flexDirection='column'>
<Text dimColor>precise = keep trailing zeros (1.0M), compact = trim them (1M / 1.1M), whole = no decimals (1M).</Text>
<Text dimColor>A global style forces that type across every widget. Decimal places are set per-widget or in settings.json.</Text>
</Box>
</Box>
);
}

if (gradientMode) {
const level = getColorLevelString(settings.colorLevel);

Expand Down Expand Up @@ -359,6 +440,12 @@ export const GlobalOverridesMenu: React.FC<GlobalOverridesMenuProps> = ({ settin
<Text dimColor> - Press (m) to toggle</Text>
</Box>

<Box>
<Text>Number Formatting: </Text>
<Text color='cyan'>{settings.numberFormat ? 'customized' : '(defaults)'}</Text>
<Text dimColor> - Press (n) to configure per-type</Text>
</Box>

<Box>
<Text> Default Padding: </Text>
<Text color='cyan'>{settings.defaultPadding ? `"${settings.defaultPadding}"` : '(none)'}</Text>
Expand Down
38 changes: 35 additions & 3 deletions src/tui/components/color-menu/__tests__/mutations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
clearAllWidgetStyling,
cycleWidgetColor,
cycleWidgetDim,
cycleWidgetNumberStyle,
resetWidgetStyling,
toggleWidgetBold,
updateWidgetById
Expand Down Expand Up @@ -58,15 +59,46 @@ describe('color-menu mutations', () => {
expect(whole[1]?.dim).toBeUndefined();
});

it('resetWidgetStyling removes color, backgroundColor, bold, and dim from one widget', () => {
it('cycleWidgetNumberStyle cycles default, compact, whole, then default for the selected widget only', () => {
const widgets: WidgetItem[] = [
{ id: '1', type: 'tokens-input' },
{ id: '2', type: 'tokens-output' }
];

const compact = cycleWidgetNumberStyle(widgets, '1');
const whole = cycleWidgetNumberStyle(compact, '1');
const off = cycleWidgetNumberStyle(whole, '1');

expect(compact[0]?.numberFormat).toEqual({ style: 'compact' });
expect(whole[0]?.numberFormat).toEqual({ style: 'whole' });
expect(off[0]).toEqual({ id: '1', type: 'tokens-input' });
expect(compact[1]?.numberFormat).toBeUndefined();
});

it('cycleWidgetNumberStyle preserves an explicit decimals across the cycle', () => {
const widgets: WidgetItem[] = [
{ id: '1', type: 'tokens-input', numberFormat: { decimals: 2 } }
];

const compact = cycleWidgetNumberStyle(widgets, '1');
const whole = cycleWidgetNumberStyle(compact, '1');
const off = cycleWidgetNumberStyle(whole, '1');

expect(compact[0]?.numberFormat).toEqual({ style: 'compact', decimals: 2 });
expect(whole[0]?.numberFormat).toEqual({ style: 'whole', decimals: 2 });
expect(off[0]?.numberFormat).toEqual({ decimals: 2 });
});

it('resetWidgetStyling removes color, backgroundColor, bold, dim, and numberFormat from one widget', () => {
const widgets: WidgetItem[] = [
{
id: '1',
type: 'tokens-input',
color: 'red',
backgroundColor: 'blue',
bold: true,
dim: 'parens'
dim: 'parens',
numberFormat: { style: 'compact' }
},
{ id: '2', type: 'tokens-output', color: 'white', bold: true }
];
Expand All @@ -87,7 +119,7 @@ describe('color-menu mutations', () => {
bold: true,
dim: true
},
{ id: '2', type: 'tokens-output', color: 'white', bold: true, dim: 'parens' }
{ id: '2', type: 'tokens-output', color: 'white', bold: true, dim: 'parens', numberFormat: { style: 'whole' } }
];

const updated = clearAllWidgetStyling(widgets);
Expand Down
40 changes: 40 additions & 0 deletions src/tui/components/color-menu/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import type {
NumberFormat,
NumberStyle
} from '../../../types/NumberFormat';
import type { WidgetItem } from '../../../types/Widget';
import { getWidget } from '../../../utils/widgets';

Expand Down Expand Up @@ -60,19 +64,53 @@ export function cycleWidgetDim(widgets: WidgetItem[], widgetId: string): WidgetI
});
}

export function cycleWidgetNumberStyle(widgets: WidgetItem[], widgetId: string): WidgetItem[] {
return updateWidgetById(widgets, widgetId, (widget) => {
// Cycle the number style: default (precise) -> compact -> whole -> default.
// Any explicit `decimals` is preserved across the cycle.
const currentStyle = widget.numberFormat?.style;
const nextStyle: NumberStyle | undefined = currentStyle === undefined
? 'compact'
: currentStyle === 'compact'
? 'whole'
: undefined;
const decimals = widget.numberFormat?.decimals;

if (nextStyle === undefined && decimals === undefined) {
const { numberFormat, ...restWidget } = widget;
void numberFormat; // Intentionally unused
return restWidget;
}

const nextFormat: NumberFormat = {};
if (nextStyle !== undefined) {
nextFormat.style = nextStyle;
}
if (decimals !== undefined) {
nextFormat.decimals = decimals;
}
return {
...widget,
numberFormat: nextFormat
};
});
}

export function resetWidgetStyling(widgets: WidgetItem[], widgetId: string): WidgetItem[] {
return updateWidgetById(widgets, widgetId, (widget) => {
const {
color,
backgroundColor,
bold,
dim,
numberFormat,
...restWidget
} = widget;
void color; // Intentionally unused
void backgroundColor; // Intentionally unused
void bold; // Intentionally unused
void dim; // Intentionally unused
void numberFormat; // Intentionally unused
return restWidget;
});
}
Expand All @@ -84,12 +122,14 @@ export function clearAllWidgetStyling(widgets: WidgetItem[]): WidgetItem[] {
backgroundColor,
bold,
dim,
numberFormat,
...restWidget
} = widget;
void color; // Intentionally unused
void backgroundColor; // Intentionally unused
void bold; // Intentionally unused
void dim; // Intentionally unused
void numberFormat; // Intentionally unused
return restWidget;
});
}
Expand Down
33 changes: 33 additions & 0 deletions src/types/NumberFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from 'zod';

// Decimal-rendering styles for numeric widgets:
// precise - fixed decimal places, trailing zeros kept ("1.0M"); today's default
// compact - trailing zeros trimmed, real fractions kept ("1M", "1.1M")
// whole - no decimals ("1M")
export const NUMBER_STYLES = ['precise', 'compact', 'whole'] as const;
export type NumberStyle = (typeof NUMBER_STYLES)[number];

// The kind of number a widget renders. Each kind keeps its own baseline
// precision in its formatter, so a token-oriented change never drags money off
// its 2-decimal convention.
export const NUMBER_KINDS = ['token', 'speed', 'percent', 'memory', 'cost'] as const;
export type NumberKind = (typeof NUMBER_KINDS)[number];

// A precision override. Both fields optional; an empty format means "use the
// formatter's built-in baseline", i.e. current output.
export const NumberFormatSchema = z.object({
style: z.enum(NUMBER_STYLES).optional(),
decimals: z.number().int().min(0).max(6).optional()
});
export type NumberFormat = z.infer<typeof NumberFormatSchema>;

// Optional global precision, keyed by number kind. A kind set here wins over any
// per-widget value (same precedence as overrideForegroundColor / globalBold).
export const GlobalNumberFormatSchema = z.object({
token: NumberFormatSchema.optional(),
speed: NumberFormatSchema.optional(),
percent: NumberFormatSchema.optional(),
memory: NumberFormatSchema.optional(),
cost: NumberFormatSchema.optional()
});
export type GlobalNumberFormat = z.infer<typeof GlobalNumberFormatSchema>;
2 changes: 2 additions & 0 deletions src/types/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod';

import { ColorLevelSchema } from './ColorLevel';
import { FlexModeSchema } from './FlexMode';
import { GlobalNumberFormatSchema } from './NumberFormat';
import { PowerlineConfigSchema } from './PowerlineConfig';
import { WidgetItemSchema } from './Widget';

Expand Down Expand Up @@ -68,6 +69,7 @@ export const SettingsSchema = z.object({
overrideBackgroundColor: z.string().optional(),
overrideForegroundColor: z.string().optional(),
globalBold: z.boolean().default(false),
numberFormat: GlobalNumberFormatSchema.optional(),
gitCacheTtlSeconds: z.number().min(0).max(60).default(5),
minimalistMode: z.boolean().default(false),
powerline: PowerlineConfigSchema.default({
Expand Down
2 changes: 2 additions & 0 deletions src/types/Widget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from 'zod';

import { NumberFormatSchema } from './NumberFormat';
import type { RenderContext } from './RenderContext';
import type { Settings } from './Settings';

Expand All @@ -11,6 +12,7 @@ export const WidgetItemSchema = z.object({
backgroundColor: z.string().optional(),
bold: z.boolean().optional(),
dim: z.union([z.boolean(), z.literal('parens')]).optional(),
numberFormat: NumberFormatSchema.optional(),
character: z.string().optional(),
rawValue: z.boolean().optional(),
customText: z.string().optional(),
Expand Down
Loading