Building tools. Learning to build tools. Learning to build learning tools.
Shared interfaces and the configuration reader that keep the extension’s modules decoupled.
Color Identity has six source files. Without a shared vocabulary, they’d either import from each
other in a tangled web or repeat type definitions. types.ts solves this by defining every
interface and the config reader in one place. Every other module imports from here; none of them
import from each other except through explicit function calls.
Let’s walk through each interface.
/** HSL color with values: h ∈ [0,360), s ∈ [0,100], l ∈ [0,100] */
export interface HSL {
h: number;
s: number;
l: number;
}
This is the extension’s internal color representation. HSL (Hue, Saturation, Lightness) is the natural choice for this project because each component maps to a meaningful axis of control:
The comment documents the value ranges using interval notation: h ∈ [0,360) means
hue goes from 0 (inclusive) to 360 (exclusive). This matters because hue 0 and hue 360 are the
same color (red), so the valid range wraps around. Saturation and lightness are simple percentages.
/** Theme profile: saturation and lightness ranges appropriate for a theme kind */
export interface ThemeProfile {
baseSaturation: number;
baseLightness: number;
fgLightness: number;
inactiveLightnessShift: number;
}
A ThemeProfile encodes the visual rules for one of VS Code’s four theme categories.
Each field answers a specific question:
| Field | Question it answers |
|---|---|
baseSaturation |
How vivid should the background tint be? |
baseLightness |
How bright should the background be? |
fgLightness |
How bright should the foreground text be? |
inactiveLightnessShift |
How much should inactive windows dim (or brighten)? |
We’ll see how these profiles are defined in Section 4. The key insight is that the same hue — say, 220° (blue) — needs very different saturation and lightness values depending on whether it’s sitting in a dark theme or a light theme.
/** Resolved set of hex colors to apply to the workspace */
export interface WorkspaceColors {
titleBarActiveBackground?: string;
titleBarActiveForeground?: string;
titleBarInactiveBackground?: string;
titleBarInactiveForeground?: string;
activityBarBackground?: string;
activityBarForeground?: string;
activityBarActiveBorder?: string;
statusBarBackground?: string;
statusBarForeground?: string;
tabsBackground?: string;
}
Every field is optional (?) because the user controls which UI elements get colorized.
If affectTitleBar is false, the four titleBar* fields stay
undefined and are never written to settings. This is how the extension respects the
user’s preferences without complex conditional logic — the type system encodes it.
These property names are camelCase versions of VS Code’s dotted key names. For example,
titleBarActiveBackground maps to titleBar.activeBackground in
workbench.colorCustomizations. The mapping is handled by colorApplier.ts
(Section 5). Using camelCase internally makes the TypeScript cleaner; the translation happens
at the boundary.
/** Configuration read from user settings */
export interface ColorIdentityConfig {
enabled: boolean;
affectTitleBar: boolean;
affectActivityBar: boolean;
affectStatusBar: boolean;
affectTabBar: boolean;
saturationAdjustment: number;
lightnessAdjustment: number;
hueOverride: number | null;
}
This is a 1:1 mirror of the package.json configuration schema from Section 1. Every
setting the user can change has a typed field here. The hueOverride field uses
number | null — null means “use the automatic hash-based hue,”
while a number means “use this specific hue.”
export function readConfig(): ColorIdentityConfig {
const cfg = vscode.workspace.getConfiguration('colorIdentity');
return {
enabled: cfg.get<boolean>('enabled', true),
affectTitleBar: cfg.get<boolean>('affectTitleBar', true),
affectActivityBar: cfg.get<boolean>('affectActivityBar', true),
affectStatusBar: cfg.get<boolean>('affectStatusBar', true),
affectTabBar: cfg.get<boolean>('affectTabBar', false),
saturationAdjustment: cfg.get<number>('saturationAdjustment', 0),
lightnessAdjustment: cfg.get<number>('lightnessAdjustment', 0),
hueOverride: cfg.get<number | null>('hueOverride', null),
};
}
vscode.workspace.getConfiguration('colorIdentity') returns a scoped config reader.
Each cfg.get() call reads one setting and provides a default if it’s not set.
The defaults here must match the defaults declared in package.json —
they’re the fallback for when the user hasn’t touched settings.
By centralizing config reading in one function, every module that needs settings calls
readConfig() and gets back a strongly-typed object. No one else touches
vscode.workspace.getConfiguration() directly. This means if the config shape
changes, there’s exactly one place to update.
You’ve seen the data contracts that hold the extension together: four interfaces and one config reader, all in 57 lines. Next, we’ll see how the color generator uses these types to turn a workspace name into a deterministic hue.