130 Widgets

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

5. Reading & Writing Workspace Settings

How to merge colors into workbench.colorCustomizations without clobbering other extensions.

The Problem

VS Code stores workspace-level UI colors in a single JSON object at workbench.colorCustomizations inside .vscode/settings.json. Multiple extensions (and the user) might be writing to this same object. If Color Identity naively overwrites the entire object, it destroys other extensions’ customizations. If it only adds its own keys, it needs a way to cleanly remove them later.

colorApplier.ts solves both problems with a managed keys pattern.

Managed Keys

const MANAGED_KEYS: readonly (keyof WorkspaceColors)[] = [
    'titleBarActiveBackground',
    'titleBarActiveForeground',
    'titleBarInactiveBackground',
    'titleBarInactiveForeground',
    'activityBarBackground',
    'activityBarForeground',
    'activityBarActiveBorder',
    'statusBarBackground',
    'statusBarForeground',
    'tabsBackground',
];

This array is the extension’s claim: “These are the keys I manage. I will add them, update them, and remove them. I will not touch anything else.” The type annotation (keyof WorkspaceColors)[] ensures this list stays in sync with the WorkspaceColors interface from types.ts — if a field is added or removed from the interface, TypeScript catches the inconsistency.

Key Mapping

const KEY_MAP: Record<keyof WorkspaceColors, string> = {
    titleBarActiveBackground: 'titleBar.activeBackground',
    titleBarActiveForeground: 'titleBar.activeForeground',
    titleBarInactiveBackground: 'titleBar.inactiveBackground',
    titleBarInactiveForeground: 'titleBar.inactiveForeground',
    activityBarBackground: 'activityBar.background',
    activityBarForeground: 'activityBar.foreground',
    activityBarActiveBorder: 'activityBar.activeBorder',
    statusBarBackground: 'statusBar.background',
    statusBarForeground: 'statusBar.foreground',
    tabsBackground: 'editorGroupHeader.tabsBackground',
};

VS Code uses dotted key names like titleBar.activeBackground in its configuration. TypeScript property names can’t contain dots, so the extension uses camelCase internally. This map translates between the two worlds. It exists in one place, used by both applyColors and resetColors.

VS Code Concept

The key editorGroupHeader.tabsBackground is worth noting — it’s not tabBar.background as you might expect. VS Code’s color key naming follows its internal component hierarchy, which doesn’t always match the user-facing terminology. When building an extension that writes color customizations, you need to consult the Theme Color Reference for the exact key names.

Applying Colors

export async function applyColors(colors: WorkspaceColors): Promise<void> {
    const config = vscode.workspace.getConfiguration();
    const existing = config.get<Record<string, string>>(
        'workbench.colorCustomizations'
    ) ?? {};

    const updated = { ...existing };

    for (const key of MANAGED_KEYS) {
        const vscodeKey = KEY_MAP[key];
        const value = colors[key];
        if (value !== undefined) {
            updated[vscodeKey] = value;
        } else {
            // If the color isn't set (element not affected), remove our old value
            delete updated[vscodeKey];
        }
    }

    await config.update(
        'workbench.colorCustomizations',
        Object.keys(updated).length > 0 ? updated : undefined,
        vscode.ConfigurationTarget.Workspace
    );
}

The merge strategy:

  1. Read existing customizations. The ?? {} handles the case where no customizations exist yet.
  2. Spread into a copy. { ...existing } creates a shallow copy so we don’t mutate the original object.
  3. Iterate only over managed keys. For each key in our list, either set it (if the color was generated) or delete it (if the user disabled that element). Keys that aren’t in MANAGED_KEYS are left completely untouched.
  4. Write back. If the result is empty, write undefined to remove the setting entirely (keeping settings.json clean). The ConfigurationTarget.Workspace ensures we only write to the workspace, never to the user’s global settings.
Warning

Always use ConfigurationTarget.Workspace when writing color customizations. Writing to Global would affect every workspace the user opens, which defeats the purpose of per-workspace identity. This is a common mistake in color-theming extensions.

Resetting Colors

export async function resetColors(): Promise<void> {
    const config = vscode.workspace.getConfiguration();
    const existing = config.get<Record<string, string>>(
        'workbench.colorCustomizations'
    ) ?? {};

    const updated = { ...existing };

    for (const key of MANAGED_KEYS) {
        delete updated[KEY_MAP[key]];
    }

    await config.update(
        'workbench.colorCustomizations',
        Object.keys(updated).length > 0 ? updated : undefined,
        vscode.ConfigurationTarget.Workspace
    );
}

Reset follows the same pattern but only deletes. Every managed key is removed; everything else survives. If the result is empty, the entire workbench.colorCustomizations key is removed from settings.json.

Architecture Note

The managed-keys pattern is a general solution for any extension that writes to shared configuration. The idea: declare which keys you own, iterate only over those, and leave the rest alone. It’s simple, correct, and easy to verify by inspection. The same pattern works for other shared VS Code settings like editor.tokenColorCustomizations.

Checkpoint

Colors are now being generated (Section 4) and applied to workspace settings (this section). Next, we’ll build the color picker UI that lets users override the automatic hue with a visual selection experience.