130 Widgets

Building tools. Learning to build tools. Learning to build learning tools.

2. Types & Configuration

Shared interfaces and the configuration reader that keep the extension’s modules decoupled.

Why a Types File?

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.

The HSL 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:

Color Science

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.

The ThemeProfile Interface

/** 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.

The WorkspaceColors Interface

/** 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.

VS Code Concept

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.

The ColorIdentityConfig Interface

/** 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 | nullnull means “use the automatic hash-based hue,” while a number means “use this specific hue.”

Reading Configuration

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.

Architecture Note

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.

Checkpoint

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.