Kingfisher is a VS Code extension that colours the status bar per workspace without writing to any shared settings file.
The VS Code extension API has no runtime mechanism to change the status bar colour. The only available mechanism is workbench.colorCustomizations, which must be written to a settings scope:
| Scope | File | Shared via VCS? |
|---|---|---|
ConfigurationTarget.Workspace |
.vscode/settings.json |
✅ Yes — ruled out |
ConfigurationTarget.WorkspaceFolder |
.vscode/settings.json |
✅ Yes — ruled out |
ConfigurationTarget.Global |
User settings (%APPDATA%\Code\User\settings.json) |
❌ No — used |
Kingfisher writes only to ConfigurationTarget.Global (user settings).
src/
extension.ts — VS Code entry point (activate/deactivate)
ColourService.ts — Pure utilities + vscode-dependent apply/persist functions
ColourPickerPanel.ts — WebviewPanel colour picker KingfisherSidebarView.ts — WebviewViewProvider sidebar panel (always-on per-window colour) test/
ColourService.test.ts — Unit tests for pure utility functions
__mocks__/
vscode.ts — Vitest mock for the vscode module
Pure utility functions (unit tested):
| Function | Description |
|---|---|
isValidHex(value) |
Validates #rrggbb or #rgb hex strings |
getContrastColour(hex) |
Returns #000000 or #ffffff using WCAG 2.x relative luminance |
dimColour(hex, factor?) |
Blends a hex colour toward mid-grey by factor (default 0.35). Used for inactive title bar states. |
buildColourCustomizations(existing, colour) |
Returns new object with all Kingfisher keys merged in (status bar + title bar) — does not mutate input |
removeColourCustomizations(existing) |
Returns new object with all Kingfisher keys removed — does not mutate input |
Keys written by buildColourCustomizations:
| Key | Value |
|---|---|
statusBar.background |
Chosen colour |
statusBar.foreground |
Auto-contrast (#000000 or #ffffff) |
titleBar.activeBackground |
Chosen colour |
titleBar.activeForeground |
Auto-contrast |
titleBar.inactiveBackground |
dimColour(colour) — blended toward mid-grey |
titleBar.inactiveForeground |
Auto-contrast of the dimmed colour |
vscode-dependent functions:
| Function | Description |
|---|---|
applyColour(colour) |
Writes to workbench.colorCustomizations via ConfigurationTarget.Global |
clearColour() |
Removes Kingfisher keys from workbench.colorCustomizations; removes the entire key if empty. No-op if no Kingfisher keys are present (idempotency guard — avoids redundant config.update() calls during shutdown). |
getSavedColour(globalState) |
Reads saved colour for current workspace from globalState keyed by workspace folder URI |
saveColour(globalState, colour) |
Persists colour for current workspace to globalState |
deleteSavedColour(globalState) |
Removes saved colour for current workspace from globalState |
Storage key: kingfisher.statusBarColour in globalState. Value is a Record<string, string> mapping workspace folder URI → hex colour.
activate(context):
- Instantiates
KingfisherSidebarViewand registers it viavscode.window.registerWebviewViewProvider - Creates a status bar item (left-aligned, low priority) bound to
kingfisher.setColour - Reads saved colour from
globalState, applies viaapplyColour, and callssidebarView.updateColour - Registers
onDidChangeWindowStatelistener:- Focus gain → reapplies the workspace colour (or clears if none saved)
- Focus loss → calls
clearColour()so unfocused windows revert to the theme default
- Registers
kingfisher.setColourcommand (QuickPick with presets + colour picker + hex input + clear option) - Registers
kingfisher.clearColourcommand
deactivate():
- Returns
Promise<void>fromclearColour()so VS Code can await the async settings write before shutdown. After blur has already cleared,clearColour()returns immediately (idempotency guard), so this is effectively a fast no-op on normal window close.
showColourPicker(context, currentColour, onApply):
- Opens a
WebviewPanel(title: "Kingfisher: Colour Picker",ViewColumn.Active) - Generates a 16-byte nonce via Node's
crypto.randomBytesfor CSP - Renders minimal HTML with:
default-src 'none'CSP with nonce-gatedstyle-srcandscript-src<input type="color">pre-filled with the current colour (or#1a6b8adefault)- Hex label that updates live on
inputevents - Apply and Cancel buttons
- On
applymessage: validates the received hex value (regex guard), disposes the panel, callsonApply(hex) - On
cancelmessage or panel close: disposes with no changes - Colour embedded in HTML is sanitised with a regex before insertion (defence-in-depth)
class KingfisherSidebarView implements vscode.WebviewViewProvider
- Registered against view ID
kingfisher.sidebarView(declared inpackage.jsonviews.kingfisher) resolveWebviewView(webviewView): called by VS Code when the panel is first opened. SetsenableScripts: true, stores a reference to the webview view, renders initial HTML.- Initial HTML: full-height page with the current colour as
background-color, a circle swatch, hex label, and a "No colour set" fallback. Uses nonce-based CSP (default-src 'none'). updateColour(hex | undefined): posts{ command: 'updateColour', colour }to the webview. The webview JS listener updatesbody.style.backgroundColorand label text in-place — no full re-render, no visible flash.- Because each VS Code window has its own extension instance and its own webview, this panel shows the correct workspace colour simultaneously and independently in every open window. This is the mechanism for true per-window Alt+Tab colouring without any shared settings file.
VS Code's user settings are global — all open windows share the same workbench.colorCustomizations value and react to it live. Kingfisher uses onDidChangeWindowState to manage this:
- Window gains focus → applies the saved colour for that workspace
- Window loses focus → calls
clearColour(), reverting to the theme default
The effective result: the active VS Code window shows its colour; all other VS Code windows visible in Alt+Tab show the default theme colours. Switching to a VS Code window immediately applies its colour.
This is an improvement over applying-on-focus-only (the previous approach), where all windows would share the colour of the last-focused window.
- Bundler: esbuild — produces
dist/extension.js(CommonJS, Node platform) - External:
vscodemodule (not bundled; provided by VS Code host) - Watch mode:
node build.mjs --watch - Config:
build.mjs
- Framework: vitest
- Test files:
src/test/**/*.test.ts - vscode mock:
src/test/__mocks__/vscode.ts— aliased viavitest.config.tsresolve.alias - Run:
npm test
Pure utility functions and clearColour are unit tested. clearColour is tested via the VS Code mock to verify the idempotency guard (no config.update() call when no Kingfisher keys are present) and correct key removal. applyColour, getSavedColour, and other vscode-dependent functions require integration with the VS Code host and are not unit tested.
- Target: ES2022
- Module: Node16 / moduleResolution: Node16
- Strict mode enabled
- Linting:
tsc --noEmit(npm run lint)
- Publisher ID:
appsoftwareltd - Extension ID:
appsoftwareltd.kingfisher - License: Elastic-2.0