mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
## Summary - ship **Buzz Term** end to end: the terminal engine/runtime, mounted desktop substrate, and user-visible naming - add Quinn's tape-deck-inspired banner: a beveled chassis filled by the `buzz term` wordmark, surrounded by a complete-hex field - derive the wordmark's three-stop sweep from each theme's terminal palette so primary, secondary, and accent roles remain visibly distinct across all 62 shipped themes, including light themes - paint the banner once on its own pointer-transparent canvas; PTY rendering beneath it remains unchanged ## Banner behavior - uses the renderer's shared `8.4 × 17` cell metrics and production aspect ratio `2.0238` - regenerates only for viewport/theme changes; palette switches repaint correctly while the banner is visible - dismisses on non-empty output from the active terminal session; empty output and inactive sessions do not dismiss it - fails closed below **70 columns** rather than squeezing or clipping the wordmark - adds **8 lines** to `terminalRenderer.ts` for shared cell metrics and **zero lines inside `paint()`** ## Screenshots | Buzz (light) | Buzz Dark | |---|---| |  |  | | Kanagawa Lotus (light) | Red | |---|---| |  |  | Additional production-aspect finals: [Vesper](https://buzz.block.builderlab.xyz/media/9ca6514b63f8cfb2107a85ca46f16a940c0883848e6fbc718e411af94aa13100.png), [Min Dark](https://buzz.block.builderlab.xyz/media/f67bd2970e5d64ffb07b1ae78ab58c847e6ebc23e7e7a48e067eb024dba64ec8.png), and [Dark Plus](https://buzz.block.builderlab.xyz/media/290fee08924f37d064abc687ecf3e9526ab05b87e8e56d610f23048949793dbe.png). The screenshot harness was checked against the shipped painter at this exact head: all **2,541 draw calls** matched on color, glyph, x, and y; four deliberate divergence controls fired. ## Verification at `98ebc8f9048bd5f0ceb7e843b67874d642f0b7fd` - desktop tests: **3,946 / 3,946** - TypeScript: clean - checks: pass (two pre-existing informational `useTemplate` notices only) - integration/e2e: PASS (independent exact-SHA lane; artifacts recorded in the originating Buzz thread) - artifact/dead-path sweep: clean - redteam G1–G7: PASS - all six named banner emitter-deletion mutants die - independent handwritten five-row full-wordmark fixture kills Quinn's seven-mutant battery, including a one-pixel glyph change - real `112 × 46` canvas-rect dismissal tests separately cover active non-empty, active empty, and inactive non-empty output - layer-drop and zero-draw painter mutants die; z-order and pointer-events verified - CI's `tsc && vite build` includes all three banner modules - performance at DPR 2 (worst-case measured envelope): - one-time content paint: **~0.7–0.8 ms**, paid only when the banner is built or its palette changes - busy compositor, CSS `1277 × 697`, backing `2554 × 1394`: **470–497 µs/frame** for the full banner (**2.82–2.98%** of a 60 Hz frame) - busy compositor, CSS `1920 × 1080`, backing `3840 × 2160`: **1,139–1,212 µs/frame** (**6.83–7.27%**) - empty, one-glyph, and full-banner controls converge: compositor cost follows backing-layer area and DPR rather than painted-cell count - in the actual idle welcome state, cost is below both vsync-clamped rigs' resolution; it is not claimed as zero - **Pane cross-rig spread: resolved at matched loop rate.** Two independent rigs initially differed 2.3× (58–68 vs 136 µs/Mpx of backing store; pane, CSS 1277×697 / backing 2554×1394, DPR 2). The cause of *that* spread is rAF loop rate: the higher figure came from a free-running loop at ~1600fps. Throttled to ~200–236fps, both rigs read 58–68 µs/Mpx (1.25–1.44% of a 60Hz frame). The busy-composite figures quoted above remain the **unthrottled worst case** and are conservative by ~2.3× at the pane. Not established: the mechanism and sign of free-running distortion (one rig under-charges ~15%, the other over-charges 2.3×), and the 1080p figure has not been re-measured throttled. - the layer paints only on generation/theme/resize and dismisses on first non-empty active-session output, so the measurable busy cost is a short-lived worst case rather than a persistent PTY paint-path tax ## Follow-ups in this PR These are intentionally subsequent commits after the certified static-banner head, not claims about `98ebc8f90`: 1. close the compositor metrology: remeasure the 1080p point throttled and characterize the opposite-sign free-running rAF distortion, with each measurement regime stated 2. add Tyler's animated honeycomb color waves, gated by `prefers-reduced-motion`, a full 62-theme phase-sweep contrast check, and DPR-2 per-tick performance certification 3. land the already-proven mounted theme-switch regression probe from `RESEARCH/BUZZ_TERM_G3A_PROBE/` 4. bound the slow/hang-shaped G1-c mutant `waitFor` 5. optionally trim the generator to its ink bounding box, reducing the minimum viewport from 70 to 62 columns --------- Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Signed-off-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@buzz.block.builderlab.xyz> Co-authored-by: npub1cc3ha7z055mu0rwwu7806t2wt8mj3pvu0uv5mfp2c50dahaqhczshdalg6 <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@buzz.block.builderlab.xyz> Co-authored-by: npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57cc <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@buzz.block.builderlab.xyz> Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz> Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
421 lines
14 KiB
TypeScript
421 lines
14 KiB
TypeScript
/**
|
|
* Theme Loader
|
|
*
|
|
* Loads Shiki theme JSON files and extracts key colors (bg, fg, comment, git).
|
|
* Only imports the theme JSON — the Shiki highlighter engine is not used here.
|
|
*/
|
|
|
|
import type { ThemeRegistrationRaw } from "shiki";
|
|
import {
|
|
type TerminalPalette,
|
|
extractTerminalPalette,
|
|
} from "./terminal-palette";
|
|
|
|
/**
|
|
* Buzz theme name. Buzz is a first-party light theme that reuses GitHub
|
|
* Light for every base color (backgrounds, text, borders, code) — the
|
|
* message area and containers are indistinguishable from GitHub Light. Its
|
|
* one distinguishing feature is a branded gradient painted across the
|
|
* sidebar/nav canvas, replacing GitHub Light's flat grey. The gradient is
|
|
* applied by {@link ThemeProvider} toggling a `data-buzz-sidebar` attribute
|
|
* on the document root; the CSS lives in `shared/styles/globals/theme.css`.
|
|
*/
|
|
export const BUZZ_THEME_NAME = "buzz";
|
|
|
|
/**
|
|
* Buzz Dark theme name. The dark-mode counterpart to {@link BUZZ_THEME_NAME}:
|
|
* reuses the GitHub Dark palette for every base color, with the same branded
|
|
* sidebar gradient (dark-tuned colors, see `shared/styles/globals/theme.css`).
|
|
* {@link ThemeProvider} toggles the shared `data-buzz-sidebar` attribute for
|
|
* this theme too; the `.dark` root class selects the dark gradient values.
|
|
*
|
|
* Buzz and Buzz Dark are paired in {@link THEME_PAIRS}, so the picker shows a
|
|
* combined "Buzz" tile under System mode (follow-OS) plus a single "Buzz" tile
|
|
* under Light and a "Buzz Dark" tile under Dark.
|
|
*/
|
|
export const BUZZ_DARK_THEME_NAME = "buzz-dark";
|
|
|
|
/** The Shiki bundle Buzz borrows its base palette from. */
|
|
export const BUZZ_BASE_THEME: SyntaxThemeName = "github-light";
|
|
|
|
/** The Shiki bundle Buzz Dark borrows its base palette from. */
|
|
export const BUZZ_DARK_BASE_THEME: SyntaxThemeName = "github-dark";
|
|
|
|
/**
|
|
* Resolve a theme name to the real Shiki bundled theme it maps to.
|
|
*
|
|
* Most themes map to themselves, but the Buzz aliases (`buzz` / `buzz-dark`)
|
|
* are not bundled Shiki themes — they reuse the GitHub Light / GitHub Dark
|
|
* palettes. The Shiki highlighter engine (used for fenced code blocks in
|
|
* `CodeBlock.tsx`) only understands bundled names, so callers that hand a
|
|
* theme name to `loadTheme` / `codeToTokens` must resolve it through here
|
|
* first; passing a raw Buzz alias makes Shiki throw and code blocks fall
|
|
* back to unhighlighted plain text.
|
|
*/
|
|
export function resolveShikiThemeName(name: string): SyntaxThemeName {
|
|
if (name === BUZZ_THEME_NAME) return BUZZ_BASE_THEME;
|
|
if (name === BUZZ_DARK_THEME_NAME) return BUZZ_DARK_BASE_THEME;
|
|
return name as SyntaxThemeName;
|
|
}
|
|
|
|
// Available themes. "buzz" is a Buzz-branded theme that reuses the
|
|
// github-light palette plus a sidebar gradient; the rest are the Shiki
|
|
// bundled syntax themes, alphabetically sorted.
|
|
export const SYNTAX_THEMES = [
|
|
"buzz",
|
|
"buzz-dark",
|
|
"andromeeda",
|
|
"aurora-x",
|
|
"ayu-dark",
|
|
"catppuccin-frappe",
|
|
"catppuccin-latte",
|
|
"catppuccin-macchiato",
|
|
"catppuccin-mocha",
|
|
"dark-plus",
|
|
"dracula",
|
|
"dracula-soft",
|
|
"everforest-dark",
|
|
"everforest-light",
|
|
"github-dark",
|
|
"github-dark-default",
|
|
"github-dark-dimmed",
|
|
"github-dark-high-contrast",
|
|
"github-light",
|
|
"github-light-default",
|
|
"github-light-high-contrast",
|
|
"gruvbox-dark-hard",
|
|
"gruvbox-dark-medium",
|
|
"gruvbox-dark-soft",
|
|
"gruvbox-light-hard",
|
|
"gruvbox-light-medium",
|
|
"gruvbox-light-soft",
|
|
"houston",
|
|
"kanagawa-dragon",
|
|
"kanagawa-lotus",
|
|
"kanagawa-wave",
|
|
"laserwave",
|
|
"light-plus",
|
|
"material-theme",
|
|
"material-theme-darker",
|
|
"material-theme-lighter",
|
|
"material-theme-ocean",
|
|
"material-theme-palenight",
|
|
"min-dark",
|
|
"min-light",
|
|
"monokai",
|
|
"night-owl",
|
|
"nord",
|
|
"one-dark-pro",
|
|
"one-light",
|
|
"plastic",
|
|
"poimandres",
|
|
"red",
|
|
"rose-pine",
|
|
"rose-pine-dawn",
|
|
"rose-pine-moon",
|
|
"slack-dark",
|
|
"slack-ochin",
|
|
"snazzy-light",
|
|
"solarized-dark",
|
|
"solarized-light",
|
|
"synthwave-84",
|
|
"tokyo-night",
|
|
"vesper",
|
|
"vitesse-black",
|
|
"vitesse-dark",
|
|
"vitesse-light",
|
|
] as const;
|
|
|
|
export type SyntaxThemeName = (typeof SYNTAX_THEMES)[number];
|
|
|
|
// Known light themes — used by the theme picker to show sun/moon icons
|
|
// for themes that haven't been loaded yet.
|
|
export const LIGHT_THEMES: ReadonlySet<SyntaxThemeName> = new Set([
|
|
"buzz",
|
|
"catppuccin-latte",
|
|
"everforest-light",
|
|
"github-light",
|
|
"github-light-default",
|
|
"github-light-high-contrast",
|
|
"gruvbox-light-hard",
|
|
"gruvbox-light-medium",
|
|
"gruvbox-light-soft",
|
|
"kanagawa-lotus",
|
|
"light-plus",
|
|
"material-theme-lighter",
|
|
"min-light",
|
|
"one-light",
|
|
"rose-pine-dawn",
|
|
"slack-ochin",
|
|
"snazzy-light",
|
|
"solarized-light",
|
|
"vitesse-light",
|
|
]);
|
|
|
|
// Static theme imports (Vite needs static strings for tree-shaking)
|
|
const themeImports: Record<
|
|
SyntaxThemeName,
|
|
() => Promise<{ default: ThemeRegistrationRaw }>
|
|
> = {
|
|
// Buzz reuses the github-light palette; its gradient is applied separately.
|
|
buzz: () => import("shiki/themes/github-light.mjs"),
|
|
// Buzz Dark reuses the github-dark palette; dark gradient applied separately.
|
|
"buzz-dark": () => import("shiki/themes/github-dark.mjs"),
|
|
andromeeda: () => import("shiki/themes/andromeeda.mjs"),
|
|
"aurora-x": () => import("shiki/themes/aurora-x.mjs"),
|
|
"ayu-dark": () => import("shiki/themes/ayu-dark.mjs"),
|
|
"catppuccin-frappe": () => import("shiki/themes/catppuccin-frappe.mjs"),
|
|
"catppuccin-latte": () => import("shiki/themes/catppuccin-latte.mjs"),
|
|
"catppuccin-macchiato": () => import("shiki/themes/catppuccin-macchiato.mjs"),
|
|
"catppuccin-mocha": () => import("shiki/themes/catppuccin-mocha.mjs"),
|
|
"dark-plus": () => import("shiki/themes/dark-plus.mjs"),
|
|
dracula: () => import("shiki/themes/dracula.mjs"),
|
|
"dracula-soft": () => import("shiki/themes/dracula-soft.mjs"),
|
|
"everforest-dark": () => import("shiki/themes/everforest-dark.mjs"),
|
|
"everforest-light": () => import("shiki/themes/everforest-light.mjs"),
|
|
"github-dark": () => import("shiki/themes/github-dark.mjs"),
|
|
"github-dark-default": () => import("shiki/themes/github-dark-default.mjs"),
|
|
"github-dark-dimmed": () => import("shiki/themes/github-dark-dimmed.mjs"),
|
|
"github-dark-high-contrast": () =>
|
|
import("shiki/themes/github-dark-high-contrast.mjs"),
|
|
"github-light": () => import("shiki/themes/github-light.mjs"),
|
|
"github-light-default": () => import("shiki/themes/github-light-default.mjs"),
|
|
"github-light-high-contrast": () =>
|
|
import("shiki/themes/github-light-high-contrast.mjs"),
|
|
"gruvbox-dark-hard": () => import("shiki/themes/gruvbox-dark-hard.mjs"),
|
|
"gruvbox-dark-medium": () => import("shiki/themes/gruvbox-dark-medium.mjs"),
|
|
"gruvbox-dark-soft": () => import("shiki/themes/gruvbox-dark-soft.mjs"),
|
|
"gruvbox-light-hard": () => import("shiki/themes/gruvbox-light-hard.mjs"),
|
|
"gruvbox-light-medium": () => import("shiki/themes/gruvbox-light-medium.mjs"),
|
|
"gruvbox-light-soft": () => import("shiki/themes/gruvbox-light-soft.mjs"),
|
|
houston: () => import("shiki/themes/houston.mjs"),
|
|
"kanagawa-dragon": () => import("shiki/themes/kanagawa-dragon.mjs"),
|
|
"kanagawa-lotus": () => import("shiki/themes/kanagawa-lotus.mjs"),
|
|
"kanagawa-wave": () => import("shiki/themes/kanagawa-wave.mjs"),
|
|
laserwave: () => import("shiki/themes/laserwave.mjs"),
|
|
"light-plus": () => import("shiki/themes/light-plus.mjs"),
|
|
"material-theme": () => import("shiki/themes/material-theme.mjs"),
|
|
"material-theme-darker": () =>
|
|
import("shiki/themes/material-theme-darker.mjs"),
|
|
"material-theme-lighter": () =>
|
|
import("shiki/themes/material-theme-lighter.mjs"),
|
|
"material-theme-ocean": () => import("shiki/themes/material-theme-ocean.mjs"),
|
|
"material-theme-palenight": () =>
|
|
import("shiki/themes/material-theme-palenight.mjs"),
|
|
"min-dark": () => import("shiki/themes/min-dark.mjs"),
|
|
"min-light": () => import("shiki/themes/min-light.mjs"),
|
|
monokai: () => import("shiki/themes/monokai.mjs"),
|
|
"night-owl": () => import("shiki/themes/night-owl.mjs"),
|
|
nord: () => import("shiki/themes/nord.mjs"),
|
|
"one-dark-pro": () => import("shiki/themes/one-dark-pro.mjs"),
|
|
"one-light": () => import("shiki/themes/one-light.mjs"),
|
|
plastic: () => import("shiki/themes/plastic.mjs"),
|
|
poimandres: () => import("shiki/themes/poimandres.mjs"),
|
|
red: () => import("shiki/themes/red.mjs"),
|
|
"rose-pine": () => import("shiki/themes/rose-pine.mjs"),
|
|
"rose-pine-dawn": () => import("shiki/themes/rose-pine-dawn.mjs"),
|
|
"rose-pine-moon": () => import("shiki/themes/rose-pine-moon.mjs"),
|
|
"slack-dark": () => import("shiki/themes/slack-dark.mjs"),
|
|
"slack-ochin": () => import("shiki/themes/slack-ochin.mjs"),
|
|
"snazzy-light": () => import("shiki/themes/snazzy-light.mjs"),
|
|
"solarized-dark": () => import("shiki/themes/solarized-dark.mjs"),
|
|
"solarized-light": () => import("shiki/themes/solarized-light.mjs"),
|
|
"synthwave-84": () => import("shiki/themes/synthwave-84.mjs"),
|
|
"tokyo-night": () => import("shiki/themes/tokyo-night.mjs"),
|
|
vesper: () => import("shiki/themes/vesper.mjs"),
|
|
"vitesse-black": () => import("shiki/themes/vitesse-black.mjs"),
|
|
"vitesse-dark": () => import("shiki/themes/vitesse-dark.mjs"),
|
|
"vitesse-light": () => import("shiki/themes/vitesse-light.mjs"),
|
|
};
|
|
|
|
export function isLightTheme(name: string): boolean {
|
|
return LIGHT_THEMES.has(name as SyntaxThemeName);
|
|
}
|
|
|
|
/**
|
|
* Theme pairs: maps a light theme to its dark counterpart and vice versa.
|
|
* Used by the "Follow system" feature to auto-switch themes.
|
|
*/
|
|
export const THEME_PAIRS: ReadonlyMap<SyntaxThemeName, SyntaxThemeName> =
|
|
new Map([
|
|
// Light → Dark
|
|
// Buzz is the first-party pair; keep it first so it leads every category.
|
|
["buzz", "buzz-dark"],
|
|
["catppuccin-latte", "catppuccin-mocha"],
|
|
["everforest-light", "everforest-dark"],
|
|
["github-light", "github-dark"],
|
|
["github-light-default", "github-dark-default"],
|
|
["github-light-high-contrast", "github-dark-high-contrast"],
|
|
["gruvbox-light-hard", "gruvbox-dark-hard"],
|
|
["gruvbox-light-medium", "gruvbox-dark-medium"],
|
|
["gruvbox-light-soft", "gruvbox-dark-soft"],
|
|
["kanagawa-lotus", "kanagawa-wave"],
|
|
["light-plus", "dark-plus"],
|
|
["material-theme-lighter", "material-theme"],
|
|
["min-light", "min-dark"],
|
|
["one-light", "one-dark-pro"],
|
|
["rose-pine-dawn", "rose-pine"],
|
|
["slack-ochin", "slack-dark"],
|
|
["solarized-light", "solarized-dark"],
|
|
["vitesse-light", "vitesse-dark"],
|
|
// Dark → Light (reverse mappings)
|
|
["buzz-dark", "buzz"],
|
|
["catppuccin-mocha", "catppuccin-latte"],
|
|
["everforest-dark", "everforest-light"],
|
|
["github-dark", "github-light"],
|
|
["github-dark-default", "github-light-default"],
|
|
["github-dark-high-contrast", "github-light-high-contrast"],
|
|
["gruvbox-dark-hard", "gruvbox-light-hard"],
|
|
["gruvbox-dark-medium", "gruvbox-light-medium"],
|
|
["gruvbox-dark-soft", "gruvbox-light-soft"],
|
|
["kanagawa-wave", "kanagawa-lotus"],
|
|
["dark-plus", "light-plus"],
|
|
["material-theme", "material-theme-lighter"],
|
|
["min-dark", "min-light"],
|
|
["one-dark-pro", "one-light"],
|
|
["rose-pine", "rose-pine-dawn"],
|
|
["slack-dark", "slack-ochin"],
|
|
["solarized-dark", "solarized-light"],
|
|
["vitesse-dark", "vitesse-light"],
|
|
]);
|
|
|
|
/**
|
|
* Get the counterpart theme for system theme switching.
|
|
* Returns the paired theme if one exists, or null if the theme has no pair.
|
|
*/
|
|
export function getThemePair(name: SyntaxThemeName): SyntaxThemeName | null {
|
|
return THEME_PAIRS.get(name) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Given a user-selected theme and the current system color scheme,
|
|
* returns the theme that should actually be applied.
|
|
*/
|
|
export function resolveSystemTheme(
|
|
selectedTheme: SyntaxThemeName,
|
|
systemIsDark: boolean,
|
|
): SyntaxThemeName {
|
|
const selectedIsLight = isLightTheme(selectedTheme);
|
|
const needsSwitch =
|
|
(systemIsDark && selectedIsLight) || (!systemIsDark && !selectedIsLight);
|
|
|
|
if (!needsSwitch) return selectedTheme;
|
|
|
|
const pair = getThemePair(selectedTheme);
|
|
return pair ?? selectedTheme;
|
|
}
|
|
|
|
// Theme settings type from Shiki
|
|
interface ThemeSetting {
|
|
scope?: string | string[];
|
|
settings?: { foreground?: string };
|
|
}
|
|
|
|
function extractCommentColor(
|
|
settings: ReadonlyArray<ThemeSetting> | undefined,
|
|
fallback: string,
|
|
): string {
|
|
if (!settings) return fallback;
|
|
|
|
for (const setting of settings) {
|
|
if (!setting.scope || !setting.settings?.foreground) continue;
|
|
const scopes = Array.isArray(setting.scope)
|
|
? setting.scope
|
|
: [setting.scope];
|
|
if (scopes.includes("comment")) {
|
|
return setting.settings.foreground;
|
|
}
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
function stripAlpha(color: string): string {
|
|
if (color.length === 9 && color.startsWith("#")) {
|
|
return color.slice(0, 7);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
function extractGitColors(colors: Record<string, string> | undefined): {
|
|
added: string | null;
|
|
deleted: string | null;
|
|
modified: string | null;
|
|
} {
|
|
if (!colors) {
|
|
return { added: null, deleted: null, modified: null };
|
|
}
|
|
|
|
const addedKeys = [
|
|
"gitDecoration.addedResourceForeground",
|
|
"editorGutter.addedBackground",
|
|
"diffEditor.insertedTextBackground",
|
|
];
|
|
const deletedKeys = [
|
|
"gitDecoration.deletedResourceForeground",
|
|
"editorGutter.deletedBackground",
|
|
"diffEditor.removedTextBackground",
|
|
];
|
|
const modifiedKeys = [
|
|
"gitDecoration.modifiedResourceForeground",
|
|
"editorGutter.modifiedBackground",
|
|
];
|
|
|
|
const findColor = (keys: string[]): string | null => {
|
|
for (const key of keys) {
|
|
const value = colors[key];
|
|
if (value) return stripAlpha(value);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
return {
|
|
added: findColor(addedKeys),
|
|
deleted: findColor(deletedKeys),
|
|
modified: findColor(modifiedKeys),
|
|
};
|
|
}
|
|
|
|
export interface ThemeInfo {
|
|
name: string;
|
|
bg: string;
|
|
fg: string;
|
|
comment: string;
|
|
added: string | null;
|
|
deleted: string | null;
|
|
modified: string | null;
|
|
terminalPalette: TerminalPalette;
|
|
}
|
|
|
|
export function extractThemeInfo(
|
|
themeName: string,
|
|
theme: ThemeRegistrationRaw,
|
|
): ThemeInfo {
|
|
const bg =
|
|
(theme.colors?.["editor.background"] as string | undefined) || "#1e1e1e";
|
|
const fg =
|
|
(theme.colors?.["editor.foreground"] as string | undefined) || "#d4d4d4";
|
|
const gitColors = extractGitColors(
|
|
theme.colors as Record<string, string> | undefined,
|
|
);
|
|
return {
|
|
name: themeName,
|
|
bg,
|
|
fg,
|
|
comment: extractCommentColor(
|
|
theme.settings as ReadonlyArray<ThemeSetting> | undefined,
|
|
fg,
|
|
),
|
|
...gitColors,
|
|
terminalPalette: extractTerminalPalette(theme),
|
|
};
|
|
}
|
|
|
|
export async function loadThemeData(
|
|
name: SyntaxThemeName,
|
|
): Promise<ThemeRegistrationRaw> {
|
|
const loader = themeImports[name];
|
|
const { default: theme } = await loader();
|
|
return theme;
|
|
}
|