themecode: Adapt, Generate, and Check Editor Themes#

themecode is a semi-latent tool: every operation is pure, offline, and deterministic – no network access, no LM calls. Where adaptation is ambiguous (a color has no classifiable role and no near palette match), the tool does not guess silently: it falls back to the nearest perceptual swatch and surfaces the decision in the report (verdict fallback, plus the unclassified-role list) for a human to review.

Subcommands:

  • themecode adapt <file> – port a foreign theme (Sublime .sublime-color-scheme, VSCode JSON, kimi-code JSON) onto the Irix palette. Each color usage walks the decision ladder: exact (already an Irix swatch, kept byte-identical) -> near (within the OKLab --threshold, snapped) -> semantic (role classified, replaced from the irix.data/semantics.yaml canon, rung-class-guarded so a surface/border-rung swatch never lands in a foreground slot) -> fallback (nearest swatch by delta_e_ok). Rewrites are textual and structure-preserving (see schemes: Theme File Format Adapters).

  • themecode generate <target> – emit an Irix-native theme for a new app (vscode, kimi) from the semantics canon. Adding a target is one function plus a GENERATORS entry.

  • themecode check <file> – static validation: parseability, hex validity, Irix coverage (share of colors that are exact swatches), and contrast floors.

Adapt, generate, and check editor themes against the Irix palette.

themecode is a semi-latent tool: every operation is pure, offline, and deterministic – no network access, no LM calls. Where adaptation is ambiguous (a color has no classifiable role and no near palette match), the tool does not guess silently: it falls back to the nearest perceptual swatch and surfaces the decision in the report (verdict fallback, plus the unclassified-role list) for a human to review.

Subcommands:

  • themecode adapt <file> – port a foreign theme (Sublime .sublime-color-scheme, VSCode JSON, kimi-code JSON) onto the Irix palette. Each color usage walks the decision ladder: exact (already an Irix swatch, kept byte-identical) -> near (within the OKLab --threshold, snapped) -> semantic (role classified, replaced from the irix.data/semantics.yaml canon, rung-class-guarded so a surface/border-rung swatch never lands in a foreground slot) -> fallback (nearest swatch by delta_e_ok). Rewrites are textual and structure-preserving (see irix.schemes).

  • themecode generate <target> – emit an Irix-native theme for a new app (vscode, kimi) from the semantics canon. Adding a target is one function plus a GENERATORS entry.

  • themecode check <file> – static validation: parseability, hex validity, Irix coverage (share of colors that are exact swatches), and contrast floors.

class irix.themecode.Decision(usage: Usage, verdict: str, new_hex: str, address: str, source_address: str = '')#

The outcome of running one Usage through the adapt decision ladder.

source_address records the pre-slide semantic address when slide_rung adjusted the rung class (empty otherwise), so the report can show [cyan-text1 ⇐ selection].

usage: Usage#

Alias for field number 0

verdict: str#

Alias for field number 1

new_hex: str#

Alias for field number 2

address: str#

Alias for field number 3

source_address: str#

Alias for field number 4

class irix.themecode.CheckReport(ok: bool, lines: list[str])#

The result of themecode check: pass flag plus human-readable report lines.

ok: bool#

Alias for field number 0

lines: list[str]#

Alias for field number 1

irix.themecode.rung_class(shade: str) → str#

Return the rung class of a shade name: surface | border | foreground.

sheet*/shape* are surfaces, bound*/line* are borders, and sign*/text* are foregrounds. Unknown shades return ''.

irix.themecode.slide_rung(address: str, kind: str) → str#

Slide a semantic address to the correct rung class for a usage, same hue family.

The semantic rung of the ladder maps a role to its canonical address regardless of how the color is used – but a surface-rung swatch (selection: cyan-bound1) placed in a foreground slot is nearly invisible, and a text-rung swatch used as a background blows out the surface. When the address’s rung class mismatches the usage’s kind, keep the hue and slide to the correct class: foreground usages always land on the hue’s text1 rung (the canonical readable-text rung – a misrunged foreground is the failure mode this guard exists to prevent, not one to approximate); surface and border usages land on the class’s nearest rung by shade order.

Parameters:
  • address – A canonical hue-shade address from the semantics table.

  • kind – The usage’s rung class ('foreground' | 'surface' | 'border'), or '' when unknown – no slide.

class irix.themecode.ThemeCoder(*, palette: Palette = <factory>, threshold: float = 0.02, semantics: Semantics = <factory>, mode: str = 'dark')#

Decision engine for themecode adapt/check: palette + threshold + semantics.

palette#

The Palette variant (dark or light) colors are adapted onto.

Type:

Palette

threshold#

OKLab delta_e_ok ceiling for the near verdict.

Type:

float

semantics#

The canonical role table from irix.data/semantics.yaml.

Type:

Semantics

mode#

Which mode’s semantics addresses to resolve (must agree with palette); drives the semantic decision-ladder step for roles carrying a per-mode {dark: ..., light: ...} override.

Type:

str

model_config = {'arbitrary_types_allowed': True}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

nearest(hex_val: str, kind: str = '') → tuple[str, str, float]#

Return (address, hex, delta_e) of the swatch nearest to hex_val.

Parameters:
  • hex_val – The color to match.

  • kind – Restrict the candidate pool to one rung class ('foreground', 'surface', 'border'); '' searches the whole palette. Kinded usages never snap to an off-class swatch – the same rung guard slide_rung gives the semantic step, applied to perceptual matching.

decide(usage: Usage) → Decision#

Run the adapt decision ladder for one usage.

Order: exact (normalized hex is already a swatch, kept as-is) -> near (delta_e_ok to the nearest swatch in the usage’s rung class is within threshold) -> semantic (the usage’s role resolves through the semantics canon, rung-class-guarded by slide_rung) -> fallback (nearest swatch in the usage’s rung class). Unkinded usages (kind == '') match the whole palette.

Parameters:

usage – The color usage to decide.

decide_all(usages: list[Usage]) → list[Decision]#

Decide every usage, in file order.

replacements(decisions: list[Decision]) → list[Replacement]#

Project decisions to per-site replacements (exact/no-change excluded).

rewrite_mapping(decisions: list[Decision]) → dict[str, str]#

Build the old-hex -> new-hex rewrite map from decisions.

exact decisions never rewrite (kept byte-identical). When several usages share one source hex but decide differently, the higher-fidelity verdict (exact > near > semantic > fallback) wins the shared rewrite.

report_lines(decisions: list[Decision]) → list[str]#

Render the per-usage adapt report (locator, role, verdict, old -> new).

check(path: Path, text: str) → CheckReport#

Statically validate a theme file against this coder’s palette.

Checks: the file parses under some adapter; every hex literal in the raw text is well-formed; Irix coverage (share of usages that are exact swatches); contrast floors – editor foreground vs. background must clear 4.5 (failure), while border/selection chrome below 3.0 is reported as a warning only (the house chrome is deliberately low-contrast).

Parameters:
  • path – File path (used for adapter detection and messages).

  • text – Raw file content.

irix.themecode.generate_vscode(palette: Palette, sem: Semantics, mode: Mode) → str#

Emit an Irix-native VSCode theme JSON document.

Parameters:
  • palette – The palette variant to resolve addresses with.

  • sem – The canonical semantics table.

  • mode – 'dark' or 'light' (the theme type field, and which per-mode override half sem.address resolves).

irix.themecode.generate_kimi(palette: Palette, sem: Semantics, mode: Mode) → str#

Emit the 19-token kimi-code theme JSON, resolved from the semantics canon.

Parameters:
  • palette – The palette variant to resolve addresses with.

  • sem – The canonical semantics table.

  • mode – 'dark' or 'light' (the theme base field, and which per-mode override half sem.address resolves).

irix.themecode.GENERATORS: dict[str, object] = {'kimi': <function generate_kimi>, 'vscode': <function generate_vscode>}#

Generation target registry – adding a target is one function plus one entry here.

irix.themecode.pick_mode(adapter: type[Adapter], text: str, usages: list[Usage]) → str#

Pick 'dark' or 'light' for a scheme whose mode was left auto.

The probe is the scheme’s main background (sublime background global, vscode editor.background): luminance < 0.5 means dark. With no background (kimi themes), the probe is the main text color instead – bright text implies a dark theme. When neither exists, dark is the documented default.

Parameters:
  • adapter – The detected format adapter.

  • text – Raw file content.

  • usages – The adapter’s parsed usages.

irix.themecode.default_output(path: Path) → Path#

Return the default adapt output path: <name>.irix.<ext> beside the input.

irix.themecode.backup(path: Path) → Path#

Copy path to <file>.bak; never clobber – timestamp the backup if one exists.

irix.themecode.main(*vargs: str) → None#

CLI entry point: dispatch the adapt / generate / check subcommands.