From f2484c0a9e8badf5c9dc24dc13bac10051375ee3 Mon Sep 17 00:00:00 2001 From: xscriptor Date: Wed, 30 Sep 2026 22:41:02 +0200 Subject: [PATCH] vscode: intelligent live tint for palettes without a bundled theme - new themesync/vscode_theme.py: base16 -> VS Code mapping (269 workbench keys, 24 token rules, 20 semantic rules), WCAG-checked foregrounds, dark/light aware, optional palette roles (accent, mauve, teal...) - vscode target: palettes bundled in xscriptor-themes keep their native theme (and the generated block is removed); any other palette gets the generated colorCustomizations over a matching Default Dark/Light Modern base, applied live by VS Code - extensions/themes/icons discovered at runtime: no palette list hardcoded - user-defined customization keys are never overwritten - scripts/check.sh validates the mapping for every palette (hex + contrast) - README target table + CHANGELOG entry --- CHANGELOG.md | 15 ++ README.md | 2 +- scripts/check.sh | 34 +++ themesync/targets/vscode.py | 211 ++++++++++++++++--- themesync/vscode_theme.py | 410 ++++++++++++++++++++++++++++++++++++ 5 files changed, 647 insertions(+), 25 deletions(-) create mode 100644 themesync/vscode_theme.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f411f0..88fea4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the equisdots theme-sync engine are documented here. Dates use YYYY-MM-DD. +## [2026-09-30] + +### Added +- VS Code target: palettes without a handcrafted theme in `xscriptor-themes` + (community/, user-created, renamed) now tint Code / Code - Insiders live + through a generated `workbench.colorCustomizations` + + `editor.tokenColorCustomizations` + `editor.semanticTokenColorCustomizations` + block (`themesync/vscode_theme.py`). The base theme is matched to the palette + luminance (Default Dark/Light Modern), bundled palettes keep their native + theme and remove the block, and user-defined customization keys are never + overwritten. Everything (themes and icons) is discovered at runtime from the + installed extension: no palette list is hardcoded. +- `scripts/check.sh`: validates the vscode mapping over every palette in the + checkout (hex colors and WCAG AA contrast). + ## [2026-09-28] ### Fixed diff --git a/README.md b/README.md index c02b307..71eee3f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Defaults target the equisdots desktop layout: | `kitty` | `~/.config/kitty/themes/.conf` + include and active border in `kitty.conf` | | `starship` | per-palette themes + fixed `~/.config/starship.toml` + `STARSHIP_CONFIG` in `.zshrc`/`.bashrc` | | `xtop` | `~/.config/xtop/themes/.jsonc` + active theme (`xtop --ct`) | -| `vscode` | `workbench.colorTheme` / `workbench.iconTheme` in Code / Code - Insiders | +| `vscode` | `workbench.colorTheme` / `workbench.iconTheme` for palettes bundled in the xscriptor-themes extension; any other palette (community/, user-created) gets an intelligent live tint via generated `colorCustomizations` + token/semantic colors, in Code / Code - Insiders | | `nvim` | `lua/themes/palettes.lua` + active-theme bootstrap | | `browsers` | Brave/Beta prefs (`color_scheme2`, accent) + Firefox `user.js` | | `opencode` | `~/.config/opencode/themes/.json` + active theme in `tui.json` | diff --git a/scripts/check.sh b/scripts/check.sh index f0ff6cc..a051d1a 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -40,6 +40,40 @@ if [ -n "$PALETTES" ]; then fi rm -f "$tmp" echo "CLI smoke test: OK ($PALETTES)" + + # vscode mapping: every palette must produce valid colors and readable + # foreground/background contrast (the dynamic tint path). + if ! PYTHONPATH="$PWD" python3 - "$PALETTES" <<'PY' +import re, sys +from pathlib import Path +from themesync import vscode_theme +from themesync.core import contrast, load_palettes, palette_bg_fg + +HEX = re.compile(r"^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$") +pals = load_palettes(Path(sys.argv[1])) +if len(pals) < 2: + print("vscode mapping: too few palettes (%d)" % len(pals)) + sys.exit(1) +for pal in pals: + out = vscode_theme.build(pal) + wb = out["workbench"] + for key, value in wb.items(): + if not HEX.match(value): + print("vscode mapping: bad color %s %s=%s" % (pal.get("slug"), key, value)) + sys.exit(1) + bg, _ = palette_bg_fg(pal) + if contrast(bg, wb["foreground"]) < 4.5: + print("vscode mapping: low contrast on %s" % pal.get("slug")) + sys.exit(1) + if len(out["tokenRules"]) < 20 or len(out["semantic"]["rules"]) < 10: + print("vscode mapping: thin token map on %s" % pal.get("slug")) + sys.exit(1) +print("vscode mapping: OK (%d palettes)" % len(pals)) +PY + then + echo "vscode mapping FAIL" + fail=1 + fi else echo "note: no palette source found, CLI smoke test skipped" >&2 fi diff --git a/themesync/targets/vscode.py b/themesync/targets/vscode.py index a8bf264..6efe122 100644 --- a/themesync/targets/vscode.py +++ b/themesync/targets/vscode.py @@ -1,57 +1,220 @@ # ═══════════════════════════════════════════════════════════════════════════ -# vscode — colorTheme + iconTheme in every installed variant. +# vscode — Code / Code - Insiders theming. # -# The xscriptor-themes extension ships one color theme AND one icon theme per -# palette (id "-icons"). workbench.colorTheme and workbench.iconTheme are -# updated in the settings.json of Code and Code - Insiders; VS Code applies them -# live. The color theme name is the capitalized slug (x → "X", bogota → "Bogotá"). +# Two regimes, decided dynamically per editor (nothing palette-specific): +# +# · palettes bundled by the xscriptor-themes extension ship a handcrafted +# theme: set workbench.colorTheme/iconTheme to it and remove any generated +# customizations (discovered by globbing the installed extension; future +# themes are picked up without touching this file). +# +# · any other palette (community/, user-created, renamed...) has no theme +# file: keep a neutral base theme for the current luminance and inject an +# intelligent `workbench.colorCustomizations` + +# `editor.tokenColorCustomizations` + `editor.semanticTokenColorCustomizations` +# generated from its base16 (themesync/vscode_theme.py). VS Code watches +# settings.json, so the change applies live in every window. +# +# The generated keys live in a marker-delimited block; switching back to a +# bundled palette removes it. User-defined customization keys are never +# overwritten: if one exists outside the block, ours for that key is skipped. # ═══════════════════════════════════════════════════════════════════════════ from __future__ import annotations +import json import re +from pathlib import Path + +from .. import vscode_theme +from ..core import load_json NAME = "vscode" -DESCRIPTION = "colorTheme + iconTheme in Code / Code - Insiders" +DESCRIPTION = "colorTheme + live tint (colorCustomizations) in Code / Code - Insiders" VARIANTS = ( ".config/Code/User/settings.json", ".config/Code - Insiders/User/settings.json", ) +# vendor dir -> extensions dir (same vendor naming as VARIANTS) +EXT_DIRS = { + "Code": ".vscode/extensions", + "Code - Insiders": ".vscode-insiders/extensions", +} + +BEGIN = "// >>> equisdots theme-sync >>>" +END = "// <<< equisdots theme-sync <<<" + +_BLOCK_RE = re.compile( + r"[ \t]*" + re.escape(BEGIN) + r".*?" + re.escape(END) + r"[ \t]*\n?", + re.S, +) +_MANAGED_KEYS = ( + "workbench.colorCustomizations", + "editor.tokenColorCustomizations", + "editor.semanticTokenColorCustomizations", +) + def available(env) -> bool: # Always: if no variant is installed, apply() does nothing. return True -def _theme_name(slug: str) -> str: - if slug == "x": - return "X" - if slug == "bogota": - return "Bogotá" - return slug[:1].upper() + slug[1:] +# ── native theme discovery (dynamic; no palette list) ───────────────────────── + +def _native(env, slug: str, vendor: str): + """Display name of the bundled theme for `slug`, or None.""" + root = Path(env.home) / EXT_DIRS.get(vendor, "") + if not root.is_dir(): + return None + for ext in sorted(root.glob("xscriptor.xscriptor-themes-*")): + theme_file = ext / "themes" / ("%s.json" % slug) + if not theme_file.is_file(): + continue + theme = load_json(theme_file, {}) or {} + return theme.get("name") or (slug[:1].upper() + slug[1:]) + return None + + +def _icon(env, slug: str, vendor: str): + """Icon theme for `slug` (falls back to the neutral 'x-icons').""" + root = Path(env.home) / EXT_DIRS.get(vendor, "") + if not root.is_dir(): + return None + for ext in sorted(root.glob("xscriptor.xscriptor-themes-*")): + pkg = load_json(ext / "package.json", {}) or {} + ids = [t.get("id") for t in (pkg.get("contributes", {}).get("iconThemes") or [])] + if ("%s-icons" % slug) in ids: + return "%s-icons" % slug + if "x-icons" in ids: + return "x-icons" + return None +# ── settings.json (JSONC) helpers ───────────────────────────────────────────── + def _set_key(text: str, key: str, value: str) -> str: - """Set a top-level JSON key (replace it or insert after the first '{').""" - pat = re.compile(r'"%s":[^,}]*' % re.escape(key)) + """Set a top-level JSON key (replace it in place or insert it).""" + encoded = json.dumps(value, ensure_ascii=False) + pat = re.compile(r'(?m)^([ \t]*)"%s"\s*:\s*[^,}\n]*' % re.escape(key)) if pat.search(text): - return pat.sub('"%s": "%s"' % (key, value), text, count=1) - return text.replace("{", '{\n "%s": "%s",' % (key, value), 1) + return pat.sub(lambda m: '%s"%s": %s' % (m.group(1), key, encoded), text, count=1) + idx = text.find("{") + if idx < 0: + return '{\n "%s": %s\n}\n' % (key, encoded) + rest = text[idx + 1:] + if rest.startswith("\n"): + rest = rest[1:] + comma = "" if rest.strip() in ("", "}") else "," + return text[:idx + 1] + '\n "%s": %s%s\n' % (key, encoded, comma) + rest + + +def _extract_object(text: str, key: str): + """The top-level object value of `key`, or (None, None) if absent/invalid.""" + m = re.search(r'"%s"\s*:\s*' % re.escape(key), text) + if not m: + return None, None + start = text.find("{", m.end()) + if start < 0: + return None, None + depth = 0 + for i in range(start, len(text)): + ch = text[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + try: + return json.loads(text[start:i + 1]), (m.start(), i + 1) + except Exception: + return None, None + return None, None + + +def _block(obj: dict, trailing_comma: bool) -> str: + """Managed block: one indented `"key": value` line per key (no wrapper).""" + parts = [] + for key, value in obj.items(): + encoded = json.dumps(value, indent=4, ensure_ascii=False) + encoded = encoded.replace("\n", "\n ") + parts.append(' "%s": %s' % (key, encoded)) + body = ",\n".join(parts) + if trailing_comma: + body += "," + return "%s\n%s\n%s" % (BEGIN, body, END) + + +def _strip_block(text: str) -> str: + """Remove the managed block and normalize a brace-line first key.""" + text = _BLOCK_RE.sub("", text) + m = re.match(r'\{\s*"', text) + if m and "\n" not in m.group(0): + text = "{\n" + text[1:].lstrip(" \t") + return text +def _insert_block(text: str, obj: dict) -> str: + idx = text.find("{") + if idx < 0: + return "{\n" + _block(obj, False) + "\n}\n" + rest = text[idx + 1:] + if rest.startswith("\n"): + rest = rest[1:] + needs_comma = rest.strip() not in ("", "}") + return text[:idx + 1] + "\n" + _block(obj, needs_comma) + "\n" + rest + + +# ── apply ───────────────────────────────────────────────────────────────────── + def apply(env) -> list: out = [] - theme = _theme_name(env.slug) - icons = "%s-icons" % env.slug for rel in VARIANTS: path = env.home / rel if not path.is_file(): continue - text = path.read_text(encoding="utf-8", errors="replace") - text = _set_key(text, "workbench.colorTheme", theme) - text = _set_key(text, "workbench.iconTheme", icons) - env.write(path, text) - variant = path.parent.parent.name # "Code" / "Code - Insiders" - out.append("vscode (%s) → color '%s', icons '%s'" % (variant, theme, icons)) + vendor = Path(rel).parent.parent.name # "Code" / "Code - Insiders" + original = path.read_text(encoding="utf-8", errors="replace") + text = _strip_block(original) + + display = _native(env, env.slug, vendor) + icon = _icon(env, env.slug, vendor) + if display: + text = _set_key(text, "workbench.colorTheme", display) + if icon: + text = _set_key(text, "workbench.iconTheme", icon) + note = "theme '%s'%s" % (display, " + '%s'" % icon if icon else "") + else: + generated = vscode_theme.build(env.palette) + payload = {} + skipped = [] + for key in _MANAGED_KEYS: + if _extract_object(text, key)[1] is not None: + skipped.append(key) + continue + if "workbench.colorCustomizations" not in skipped: + payload["workbench.colorCustomizations"] = generated["workbench"] + if "editor.tokenColorCustomizations" not in skipped: + payload["editor.tokenColorCustomizations"] = { + "textMateRules": generated["tokenRules"], + } + if "editor.semanticTokenColorCustomizations" not in skipped: + payload["editor.semanticTokenColorCustomizations"] = generated["semantic"] + + base = "Default Light Modern" if env.light else "Default Dark Modern" + text = _set_key(text, "workbench.colorTheme", base) + if icon: + text = _set_key(text, "workbench.iconTheme", icon) + if payload: + text = _insert_block(text, payload) + note = "live tint for '%s' over '%s' (%d workbench keys, %d token rules%s)" % ( + env.slug, base, + len(generated["workbench"]), len(generated["tokenRules"]), + "; skipped user keys: %s" % ", ".join(skipped) if skipped else "", + ) + + if text != original: + env.write(path, text) + out.append("vscode (%s) → %s" % (vendor, note)) return out diff --git a/themesync/vscode_theme.py b/themesync/vscode_theme.py new file mode 100644 index 0000000..2d885dd --- /dev/null +++ b/themesync/vscode_theme.py @@ -0,0 +1,410 @@ +# ═══════════════════════════════════════════════════════════════════════════ +# vscode_theme — intelligent base16 → VS Code color mapping. +# +# Generates `workbench.colorCustomizations`, `editor.tokenColorCustomizations` +# and `editor.semanticTokenColorCustomizations` for ANY palette, including +# community/ and user-created ones. Nothing here is palette-specific: the +# mapping is derived from the palette's base16 slots (optionally refined by +# its `roles`, using the same role names as the shell: accent, blue, green, +# red, mauve, teal...). +# +# Rules: +# · surfaces are mixed from background→foreground (works for dark AND light +# palettes: mixing toward the fg always increases separation), +# · selection/hover derive from the accent, +# · every foreground is contrast-checked (WCAG) against its background, +# · text on accents picks whichever of bg/fg contrasts most. +# ═══════════════════════════════════════════════════════════════════════════ +from __future__ import annotations + +from .core import best_fg, contrast, luminance, mix, palette_bg_fg, palette_hex + +MIN_CONTRAST = 4.5 + + +def _readable(bg: str, prefer: str) -> str: + """`prefer` if it clears WCAG AA on `bg`, else the best of white/black.""" + if contrast(bg, prefer) >= MIN_CONTRAST: + return prefer + return max([prefer, "#ffffff", "#000000"], key=lambda c: contrast(bg, c)) + + +def build(pal: dict) -> dict: + """All three customizations for a palette dict (base16 + optional roles).""" + bg, fg_raw = palette_bg_fg(pal) + fg = _readable(bg, fg_raw) + dark = luminance(bg) <= 0.5 + roles = pal.get("roles") or {} + + def slot(n: int) -> str: + return palette_hex(pal, "color%d" % n) + + def role(name: str, fallback: str) -> str: + value = roles.get(name) + return value if isinstance(value, str) and value.strip() else fallback + + red = role("red", slot(1)) + green = role("green", slot(2)) + yellow = role("yellow", slot(3)) + blue = role("blue", slot(4)) + mauve = role("mauve", slot(5)) + cyan = role("teal", slot(6)) + peach = role("peach", slot(9)) + pink = role("pink", slot(13)) + accent = role("accent", mauve) + accent2 = role("accent2", blue) + + dim = mix(fg, bg, 0.35) + faint = mix(fg, bg, 0.55) + s0 = mix(bg, fg, 0.05) + s1 = mix(bg, fg, 0.10) + s2 = mix(bg, fg, 0.17) + overlay = mix(bg, fg, 0.30) + sel = mix(bg, accent, 0.35) + sel_inactive = mix(bg, accent, 0.18) + hover = mix(bg, fg, 0.09) + shadow = "#00000080" if dark else "#00000026" + on_accent = best_fg(accent, bg, fg) + on_accent2 = best_fg(accent2, bg, fg) + + workbench = { + # base + "foreground": fg, + "descriptionForeground": dim, + "errorForeground": red, + "icon.foreground": fg, + "focusBorder": accent, + "selection.background": sel, + "widget.border": s1, + "widget.shadow": shadow, + "sash.hoverBorder": accent, + + # editor surface + "editor.background": bg, + "editor.foreground": fg, + "editorCursor.foreground": accent, + "editor.lineHighlightBackground": s0, + "editor.lineHighlightBorder": "#00000000", + "editor.selectionBackground": sel, + "editor.inactiveSelectionBackground": sel_inactive, + "editor.selectionHighlightBackground": mix(bg, accent, 0.25), + "editor.selectionHighlightBorder": "#00000000", + "editor.wordHighlightBackground": mix(bg, blue, 0.22), + "editor.wordHighlightStrongBackground": mix(bg, accent2, 0.22), + "editor.findMatchBackground": mix(bg, yellow, 0.45), + "editor.findMatchHighlightBackground": mix(bg, yellow, 0.25), + "editor.findRangeHighlightBackground": s0, + "editor.rangeHighlightBackground": s0, + "editor.hoverHighlightBackground": mix(bg, accent, 0.15), + "editorLineNumber.foreground": faint, + "editorLineNumber.activeForeground": accent, + "editorIndentGuide.background1": s1, + "editorIndentGuide.activeBackground1": overlay, + "editorWhitespace.foreground": s2, + "editorRuler.foreground": s1, + "editorCodeLens.foreground": faint, + "editorLink.activeForeground": blue, + "editorBracketMatch.background": mix(bg, accent, 0.20), + "editorBracketMatch.border": accent, + "editorBracketHighlight.foreground1": red, + "editorBracketHighlight.foreground2": yellow, + "editorBracketHighlight.foreground3": green, + "editorBracketHighlight.foreground4": cyan, + "editorBracketHighlight.foreground5": blue, + "editorBracketHighlight.foreground6": mauve, + "editorGutter.background": bg, + "editorGutter.modifiedBackground": yellow, + "editorGutter.addedBackground": green, + "editorGutter.deletedBackground": red, + "editorError.foreground": red, + "editorWarning.foreground": yellow, + "editorInfo.foreground": blue, + "editorHint.foreground": cyan, + "editorOverviewRuler.border": s1, + "editorOverviewRuler.errorForeground": red, + "editorOverviewRuler.warningForeground": yellow, + "editorOverviewRuler.infoForeground": blue, + + # widgets + "editorWidget.background": s0, + "editorWidget.foreground": fg, + "editorWidget.border": s1, + "editorHoverWidget.background": s0, + "editorHoverWidget.foreground": fg, + "editorHoverWidget.border": s1, + "editorSuggestWidget.background": s0, + "editorSuggestWidget.foreground": fg, + "editorSuggestWidget.border": s1, + "editorSuggestWidget.selectedBackground": sel, + "editorSuggestWidget.highlightForeground": accent, + "editorSuggestWidget.focusHighlightForeground": accent2, + "editorSuggestWidgetStatus.foreground": dim, + "editorMarkerNavigation.background": s0, + "editorMarkerNavigationError.background": red, + "editorMarkerNavigationWarning.background": yellow, + "editorMarkerNavigationInfo.background": blue, + "problemsErrorIcon.foreground": red, + "problemsWarningIcon.foreground": yellow, + "problemsInfoIcon.foreground": blue, + + # workbench chrome + "activityBar.background": s0, + "activityBar.foreground": fg, + "activityBar.inactiveForeground": faint, + "activityBar.border": s1, + "activityBar.activeBorder": accent, + "activityBar.activeBackground": mix(bg, accent, 0.14), + "activityBar.dropBorder": accent, + "activityBarBadge.background": accent, + "activityBarBadge.foreground": on_accent, + "sideBar.background": bg, + "sideBar.foreground": fg, + "sideBar.border": s1, + "sideBarTitle.foreground": fg, + "sideBarSectionHeader.background": s0, + "sideBarSectionHeader.foreground": fg, + "sideBarSectionHeader.border": s1, + "panel.background": bg, + "panel.border": s1, + "panelTitle.activeForeground": fg, + "panelTitle.activeBorder": accent, + "panelTitle.inactiveForeground": faint, + "panelInput.border": s1, + "panelSection.border": s1, + "panelSectionHeader.background": s0, + "statusBar.background": s1, + "statusBar.foreground": fg, + "statusBar.border": s1, + "statusBar.noFolderBackground": s0, + "statusBarItem.hoverBackground": s2, + "statusBarItem.remoteBackground": accent, + "statusBarItem.remoteForeground": on_accent, + "statusBarItem.errorBackground": red, + "statusBarItem.errorForeground": best_fg(red, bg, fg), + "statusBarItem.warningBackground": yellow, + "statusBarItem.warningForeground": best_fg(yellow, bg, fg), + "titleBar.activeBackground": s0, + "titleBar.activeForeground": fg, + "titleBar.inactiveBackground": s0, + "titleBar.inactiveForeground": faint, + "titleBar.border": s1, + "menu.background": s0, + "menu.foreground": fg, + "menu.border": s1, + "menu.selectionBackground": sel, + "menu.selectionForeground": fg, + "menu.separatorBackground": s1, + "menubar.selectionBackground": s1, + "menubar.selectionForeground": fg, + + # tabs / groups + "editorGroup.border": s1, + "editorGroup.dropBackground": sel_inactive, + "editorGroupHeader.tabsBackground": s0, + "editorGroupHeader.tabsBorder": s1, + "editorGroupHeader.noTabsBackground": s0, + "tab.activeBackground": bg, + "tab.activeForeground": fg, + "tab.activeBorderTop": accent, + "tab.activeBorder": "#00000000", + "tab.inactiveBackground": s0, + "tab.inactiveForeground": faint, + "tab.border": s1, + "tab.hoverBackground": s1, + "tab.unfocusedActiveBackground": s0, + "tab.unfocusedInactiveForeground": faint, + "tab.lastPinnedBorder": s1, + + # lists / inputs / buttons + "list.activeSelectionBackground": sel, + "list.activeSelectionForeground": fg, + "list.inactiveSelectionBackground": mix(bg, fg, 0.12), + "list.inactiveSelectionForeground": fg, + "list.hoverBackground": hover, + "list.hoverForeground": fg, + "list.focusBackground": sel, + "list.focusForeground": fg, + "list.focusOutline": accent, + "list.highlightForeground": accent, + "list.errorForeground": red, + "list.warningForeground": yellow, + "list.deemphasizedForeground": faint, + "input.background": s0, + "input.foreground": fg, + "input.border": s1, + "input.placeholderForeground": faint, + "inputOption.activeBorder": accent, + "inputOption.activeBackground": mix(bg, accent, 0.20), + "inputValidation.errorBackground": mix(bg, red, 0.25), + "inputValidation.errorBorder": red, + "inputValidation.warningBackground": mix(bg, yellow, 0.25), + "inputValidation.warningBorder": yellow, + "inputValidation.infoBackground": mix(bg, blue, 0.25), + "inputValidation.infoBorder": blue, + "dropdown.background": s0, + "dropdown.foreground": fg, + "dropdown.border": s1, + "button.background": accent, + "button.foreground": on_accent, + "button.hoverBackground": mix(accent, fg, 0.15), + "button.secondaryBackground": s1, + "button.secondaryForeground": fg, + "badge.background": accent, + "badge.foreground": on_accent, + "progressBar.background": accent, + "checkbox.background": s0, + "checkbox.foreground": fg, + "checkbox.border": s1, + + # scrollbars / minimap / diff / misc + "scrollbar.shadow": shadow, + "scrollbarSlider.background": mix(bg, fg, 0.18), + "scrollbarSlider.hoverBackground": mix(bg, fg, 0.28), + "scrollbarSlider.activeBackground": mix(bg, fg, 0.35), + "minimap.background": bg, + "minimap.selectionHighlight": sel, + "minimap.findMatchHighlight": mix(bg, yellow, 0.45), + "quickInput.background": s0, + "quickInput.foreground": fg, + "quickInputList.focusBackground": sel, + "quickInputList.focusForeground": fg, + "peekView.border": accent, + "peekViewEditor.background": s0, + "peekViewEditor.matchHighlightBackground": mix(bg, yellow, 0.30), + "peekViewResult.background": bg, + "peekViewResult.selectionBackground": sel, + "peekViewTitle.background": s0, + "breadcrumb.foreground": dim, + "breadcrumb.focusForeground": fg, + "breadcrumb.activeSelectionForeground": accent, + "breadcrumbPicker.background": s0, + "gitDecoration.addedResourceForeground": green, + "gitDecoration.modifiedResourceForeground": yellow, + "gitDecoration.deletedResourceForeground": red, + "gitDecoration.untrackedResourceForeground": green, + "gitDecoration.ignoredResourceForeground": faint, + "gitDecoration.conflictingResourceForeground": red, + "diffEditor.insertedTextBackground": mix(bg, green, 0.18), + "diffEditor.removedTextBackground": mix(bg, red, 0.18), + "diffEditor.insertedLineBackground": mix(bg, green, 0.10), + "diffEditor.removedLineBackground": mix(bg, red, 0.10), + "notifications.background": s0, + "notifications.foreground": fg, + "notifications.border": s1, + "notificationCenterHeader.background": s1, + "notificationLink.foreground": blue, + "extensionButton.prominentBackground": accent, + "extensionButton.prominentForeground": on_accent, + "extensionBadge.remoteBackground": accent, + "extensionBadge.remoteForeground": on_accent, + "settings.headerForeground": fg, + "settings.modifiedItemIndicator": accent, + "textLink.foreground": blue, + "textLink.activeForeground": cyan, + "textPreformat.foreground": peach, + "textBlockQuote.background": s0, + "textBlockQuote.border": s1, + "textCodeBlock.background": s0, + "pickerGroup.border": s1, + "pickerGroup.foreground": accent, + "debugToolBar.background": s0, + "debugConsole.infoForeground": blue, + "debugConsole.warningForeground": yellow, + "debugConsole.errorForeground": red, + "debugConsole.sourceForeground": dim, + "debugIcon.breakpointForeground": red, + "debugIcon.breakpointCurrentStackframeForeground": yellow, + "ports.iconRunningProcessForeground": green, + "testing.iconPassed": green, + "testing.iconFailed": red, + "testing.iconQueued": yellow, + "testing.iconUnset": faint, + "testing.iconSkipped": pink, + "charts.red": red, + "charts.blue": blue, + "charts.yellow": yellow, + "charts.orange": peach, + "charts.green": green, + "charts.purple": mauve, + "charts.foreground": fg, + "charts.lines": dim, + + # terminal + "terminal.background": bg, + "terminal.foreground": fg, + "terminalCursor.foreground": accent, + "terminalCursor.background": bg, + "terminal.selectionBackground": sel, + "terminal.inactiveSelectionBackground": sel_inactive, + "terminal.ansiBlack": slot(0), + "terminal.ansiRed": slot(1), + "terminal.ansiGreen": slot(2), + "terminal.ansiYellow": slot(3), + "terminal.ansiBlue": slot(4), + "terminal.ansiMagenta": slot(5), + "terminal.ansiCyan": slot(6), + "terminal.ansiWhite": slot(7), + "terminal.ansiBrightBlack": slot(8), + "terminal.ansiBrightRed": slot(9), + "terminal.ansiBrightGreen": slot(10), + "terminal.ansiBrightYellow": slot(11), + "terminal.ansiBrightBlue": slot(12), + "terminal.ansiBrightMagenta": slot(13), + "terminal.ansiBrightCyan": slot(14), + "terminal.ansiBrightWhite": slot(15), + } + + token_rules = [ + {"scope": ["comment", "punctuation.definition.comment"], "settings": {"foreground": faint, "fontStyle": "italic"}}, + {"scope": ["keyword", "storage", "storage.type", "storage.modifier", "keyword.control", "keyword.operator.new", "variable.language"], "settings": {"foreground": mauve}}, + {"scope": ["string", "string.quoted", "punctuation.definition.string"], "settings": {"foreground": green}}, + {"scope": ["string.regexp", "constant.regexp"], "settings": {"foreground": cyan}}, + {"scope": ["constant.numeric", "constant.language", "constant.character", "support.constant", "constant.other"], "settings": {"foreground": peach}}, + {"scope": ["entity.name.function", "support.function", "meta.function-call", "meta.function-call.generic"], "settings": {"foreground": blue}}, + {"scope": ["entity.name.type", "entity.name.class", "entity.name.namespace", "support.type", "support.class", "entity.other.inherited-class"], "settings": {"foreground": cyan}}, + {"scope": ["entity.name.tag", "punctuation.definition.tag"], "settings": {"foreground": red}}, + {"scope": ["entity.other.attribute-name", "meta.object-literal.key"], "settings": {"foreground": yellow}}, + {"scope": ["variable", "variable.other", "meta.definition.variable"], "settings": {"foreground": fg}}, + {"scope": ["variable.parameter"], "settings": {"foreground": peach}}, + {"scope": ["meta.property-name", "variable.other.property", "support.variable.property"], "settings": {"foreground": blue}}, + {"scope": ["keyword.operator", "punctuation.separator", "punctuation.terminator"], "settings": {"foreground": dim}}, + {"scope": ["markup.heading", "markup.heading entity.name"], "settings": {"foreground": accent, "fontStyle": "bold"}}, + {"scope": ["markup.bold"], "settings": {"fontStyle": "bold"}}, + {"scope": ["markup.italic"], "settings": {"fontStyle": "italic"}}, + {"scope": ["markup.inserted", "markup.inserted.git_gutter"], "settings": {"foreground": green}}, + {"scope": ["markup.deleted", "markup.deleted.git_gutter"], "settings": {"foreground": red}}, + {"scope": ["markup.changed"], "settings": {"foreground": yellow}}, + {"scope": ["markup.quote"], "settings": {"foreground": dim, "fontStyle": "italic"}}, + {"scope": ["markup.raw", "markup.inline.raw"], "settings": {"foreground": green}}, + {"scope": ["markup.link", "markup.underline.link"], "settings": {"foreground": blue}}, + {"scope": ["invalid", "invalid.illegal"], "settings": {"foreground": red}}, + {"scope": ["invalid.deprecated"], "settings": {"foreground": peach}}, + ] + + semantic = { + "enabled": True, + "rules": { + "keyword": {"foreground": mauve}, + "operator": {"foreground": accent}, + "namespace": {"foreground": cyan}, + "type": {"foreground": cyan}, + "class": {"foreground": cyan}, + "struct": {"foreground": cyan}, + "interface": {"foreground": cyan}, + "enum": {"foreground": cyan}, + "enumMember": {"foreground": peach}, + "function": {"foreground": blue}, + "method": {"foreground": blue}, + "property": {"foreground": blue}, + "variable": {"foreground": fg}, + "parameter": {"foreground": peach}, + "string": {"foreground": green}, + "number": {"foreground": peach}, + "comment": {"foreground": faint, "italic": True}, + "decorator": {"foreground": accent2}, + "*.deprecated": {"foreground": peach, "strikethrough": True}, + "*.declaration": {"bold": False}, + }, + } + + return {"workbench": workbench, "tokenRules": token_rules, "semantic": semantic}