schemes: Theme File Format Adapters#

Each adapter understands one foreign (or native) theme file format – Sublime .sublime-color-scheme (JSONC), VSCode theme JSON, and the kimi-code 19-token TUI theme JSON – and exposes a uniform surface:

  • detect(path, text) – extension + content sniffing.

  • usages(text) – every color usage in the file: a JSON-path-ish locator, the canonical role key from irix.semantics.Semantics (or None), and the original hex literal.

  • main_background(text) – the scheme’s principal background hex, for mode auto-detection.

Rewriting is textual and structure-preserving: hex literals are replaced in the raw text, never by re-serializing parsed JSON, so comments and formatting in JSONC files survive intact. Adapter.rewrite applies a global hex mapping; Adapter.rewrite_decisions goes further and rewrites per site (unique keys by anchored match, token-rule lists by channel-sequential cursor), so two usages that share one literal but decide differently – the norm in real flat-JSON themes like Dracula’s, which reuses #6272A4 for twenty distinct roles – each get their own decision. For Sublime schemes, var(...) references are resolved through the scheme’s own variables: dict for classification; the replacement lands at the variable’s definition site, inheriting the highest-fidelity decision among the variable and its referrers.

Theme File Format Adapters.

Each adapter understands one foreign (or native) theme file format – Sublime .sublime-color-scheme (JSONC), VSCode theme JSON, and the kimi-code 19-token TUI theme JSON – and exposes a uniform surface:

  • detect(path, text) – extension + content sniffing.

  • usages(text) – every color usage in the file: a JSON-path-ish locator, the canonical role key from irix.semantics.Semantics (or None), and the original hex literal.

  • main_background(text) – the scheme’s principal background hex, for mode auto-detection.

Rewriting is textual and structure-preserving: hex literals are replaced in the raw text, never by re-serializing parsed JSON, so comments and formatting in JSONC files survive intact. Adapter.rewrite applies a global hex mapping; Adapter.rewrite_decisions goes further and rewrites per site (unique keys by anchored match, token-rule lists by channel-sequential cursor), so two usages that share one literal but decide differently – the norm in real flat-JSON themes like Dracula’s, which reuses #6272A4 for twenty distinct roles – each get their own decision. For Sublime schemes, var(...) references are resolved through the scheme’s own variables: dict for classification; the replacement lands at the variable’s definition site, inheriting the highest-fidelity decision among the variable and its referrers.

irix.schemes.HEX_RE = re.compile('#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3})(?![0-9a-fA-F])')#

Matches hex color literals – #RGB, #RRGGBB, or #RRGGBBAA (case-insensitive), refusing partial matches of longer hex runs.

irix.schemes.KIMI_TOKENS: dict[str, str] = {'accent': 'ui.accent', 'border': 'ui.border', 'borderFocus': 'ui.border-focus', 'diffAdded': 'diff.added', 'diffAddedStrong': 'diff.added-strong', 'diffGutter': 'diff.gutter', 'diffMeta': 'diff.meta', 'diffRemoved': 'diff.removed', 'diffRemovedStrong': 'diff.removed-strong', 'error': 'ui.error', 'primary': 'ui.primary', 'roleUser': 'role.user', 'shellMode': 'mode.shell', 'success': 'ui.success', 'text': 'ui.text', 'textDim': 'ui.text-dim', 'textMuted': 'ui.text-muted', 'textStrong': 'ui.text-strong', 'warning': 'ui.warning'}#

The kimi-code theme’s fixed 19 tokens -> canonical roles.

class irix.schemes.Usage(locator: str, role: str | None, hex: str, kind: str = '')#

Bases: NamedTuple

One color usage in a theme file.

locator is a JSON-path-ish human-readable address (e.g. rules[12].foreground, colors.editor.background, variables.sand-1); role is the canonical role key from Semantics classification or None; hex is the original literal as written; kind is the usage’s rung class – 'foreground', 'surface', 'border', or '' when unknown (e.g. a Sublime variables: definition) – and guards the semantic decision rung against cross-class mappings (see irix.themecode.slide_rung).

locator: str#

Alias for field number 0

role: str | None#

Alias for field number 1

hex: str#

Alias for field number 2

kind: str#

Alias for field number 3

class irix.schemes.Replacement(locator: str, old: str, new: str, verdict: str)#

Bases: NamedTuple

One decided rewrite: which usage, from which literal, to which new hex.

old is the literal exactly as written at the usage’s site (case and alpha preserved); new is the normalized #RRGGBB decision; verdict lets per-site rewriters resolve definition-site conflicts by ladder fidelity.

locator: str#

Alias for field number 0

old: str#

Alias for field number 1

new: str#

Alias for field number 2

verdict: str#

Alias for field number 3

irix.schemes.normalize_hex(value: str) → str#

Normalize a hex literal to uppercase 6-digit #RRGGBB form (alpha dropped).

Parameters:

value – A #RGB, #RRGGBB, or #RRGGBBAA literal, any case.

irix.schemes.is_hex(value: str) → bool#

Return True if value is exactly a hex color literal (3/6/8 digits).

irix.schemes.kind_of_key(key: str) → str#

Infer a color usage’s rung class (kind) from its workbench/global key name.

Keys mentioning foreground are foregrounds, background keys are surfaces, and keys mentioning border/outline are borders; anything else is unknown (''). Substring (not just suffix) matching, so numbered keys like editorBracketHighlight.foreground1 still classify.

Parameters:

key – A workbench key (editor.background, sideBar.border, …).

irix.schemes.strip_jsonc(text: str) → str#

Strip // and /* */ comments plus trailing commas from JSONC text.

String-aware: comment markers inside string literals (e.g. URLs) are preserved. For analysis only – rewrites never use the stripped form.

Parameters:

text – Raw JSONC file content.

Returns:

Text parseable by json.loads.

class irix.schemes.Adapter#

Bases: object

Base class for theme format adapters; holds the shared textual rewrite.

classmethod detect(path: Path, text: str) → bool#

Return True if this adapter claims the file at path with content text.

classmethod usages(text: str, sem: Semantics) → list[Usage]#

Return every color usage in text, classified through sem.

classmethod main_background(text: str) → str | None#

Return the scheme’s principal background hex (for mode auto-detection).

classmethod rewrite(text: str, mapping: dict[str, str]) → str#

Replace hex literals in text per mapping, preserving all other bytes.

Parameters:
  • text – The raw file content.

  • mapping – Normalized old hex (#RRGGBB upper) -> new #RRGGBB hex. 8-digit literals keep their original alpha suffix; 3-digit literals are expanded to the 6-digit replacement.

classmethod rewrite_decisions(text: str, replacements: list[Replacement]) → str#

Apply per-usage decisions to text, preserving all other bytes.

The base implementation degrades to the global hex mapping (first decision per hex wins); format adapters override it with per-site rewrites so that two usages sharing one literal but deciding differently each get their own value.

class irix.schemes.SublimeAdapter#

Bases: Adapter

Adapter for Sublime Text .sublime-color-scheme files (JSONC).

classmethod detect(path: Path, text: str) → bool#

Claim .sublime-color-scheme files, or JSONC with a rules array.

classmethod usages(text: str, sem: Semantics) → list[Usage]#

Yield usages from variables, globals, and rules (var-resolved).

classmethod main_background(text: str) → str | None#

Return the background global (falling back to a background variable).

classmethod rewrite_decisions(text: str, replacements: list[Replacement]) → str#

Apply decisions per site: variable definitions, literal globals, inline rules.

var(...)-referencing rules and globals carry no literal of their own, so their decisions are inherited by the variable’s definition site: each variable rewrites to the highest-fidelity decision among itself and everything that references it (its own decision wins ties). Rules with inline hex literals are rewritten channel-sequentially within the rules array, so two rules sharing one literal still get their own values. Comments and formatting are untouched.

class irix.schemes.VscodeAdapter#

Bases: Adapter

Adapter for VSCode theme JSON (colors + tokenColors + semanticTokenColors).

classmethod detect(path: Path, text: str) → bool#

Claim JSON files with a tokenColors array or an editor.background color.

classmethod usages(text: str, sem: Semantics) → list[Usage]#

Yield usages from colors, tokenColors, and semanticTokenColors.

classmethod main_background(text: str) → str | None#

Return colors['editor.background'], else the theme type hint is unused.

classmethod rewrite_decisions(text: str, replacements: list[Replacement]) → str#

Apply decisions per site: unique colors keys, then token rules in order.

Flat workbench keys are unique, so each rewrites by anchored key match. tokenColors rules repeat literals across rules, so foreground/background channels rewrite sequentially (rule i’s decision lands on rule i) within the tokenColors section only, never bleeding into semanticTokenColors.

class irix.schemes.KimiAdapter#

Bases: Adapter

Adapter for the kimi-code 19-token flat colors theme JSON.

classmethod detect(path: Path, text: str) → bool#

Claim JSON files whose colors dict carries the kimi token signature.

classmethod usages(text: str, sem: Semantics) → list[Usage]#

Yield one usage per kimi token (roles come from the fixed KIMI_TOKENS map).

classmethod main_background(text: str) → str | None#

Return None – kimi themes carry no background; mode detection uses text.

classmethod text_probe(text: str) → str | None#

Return the text token hex, the best luminance probe for a kimi theme.

classmethod rewrite_decisions(text: str, replacements: list[Replacement]) → str#

Apply decisions per token key (the 19 kimi tokens are unique).

irix.schemes.ADAPTERS: tuple[type[Adapter], ...] = (<class 'irix.schemes.SublimeAdapter'>, <class 'irix.schemes.KimiAdapter'>, <class 'irix.schemes.VscodeAdapter'>)#

Adapter registry, checked in order (kimi before vscode – its signature is narrower).

irix.schemes.detect_adapter(path: Path, text: str) → type[Adapter] | None#

Return the first adapter that claims path/text, or None.