Skip to content

Instantly share code, notes, and snippets.

@UrsaDK
Last active February 28, 2026 19:45
Show Gist options
  • Select an option

  • Save UrsaDK/a4a61f5602dca65bfb22d2908aa98134 to your computer and use it in GitHub Desktop.

Select an option

Save UrsaDK/a4a61f5602dca65bfb22d2908aa98134 to your computer and use it in GitHub Desktop.
A dynamic Blink terminal theme that matches system appearance (dark and light).
(function () {
const LIGHT_THEME = {
'name': 'GitHub Light (Primer)',
'canvas': {
'foreground': '#1f2328',
'background': '#ffffff'
},
'cursor': {
'background': 'rgba(31,35,40,0.5)'
},
'palette': {
'black': '#1f2328',
'red': '#a40e26', // a40e26 cf222e fa4549 ff8182
'green': '#2da44e', // 1a7f37 2da44e 4ac26b 6fdd8b
'yellow': '#d4a72c', // 9a6700 bf8700 d4a72c eac54f
'blue': '#0969da',
'magenta': '#8250df',
'cyan': '#3192aa', // 1b7c83 3192aa 6cb6ff
'white': '#c9d1d9', // d0d7de eaeef2
'brightBlack': '#30363d',
'brightRed': '#cf222e',
'brightGreen': '#4ac26b',
'brightYellow': '#eac54f',
'brightBlue': '#2188ff',
'brightMagenta': '#a475e6',
'brightCyan': '#35adb9',
'brightWhite': '#f6f8fa',
}
};
const DARK_THEME = {
'name': 'GitHub Dark (Primer)',
'canvas': {
'foreground': '#c9d1d9',
'background': '#0d1117'
},
'cursor': {
'background': 'rgba(201,209,217,0.5)'
},
'palette': {
'black': '#161b22',
'red': '#da3633',
'green': '#2ea043',
'yellow': '#bf8700',
'blue': '#388bfd',
'magenta': '#a371f7',
'cyan': '#39c5cf',
'white': '#d0d7de', // c9d1d9 e6edf3
'brightBlack': '#30363d',
'brightRed': '#f85149',
'brightGreen': '#3fb950',
'brightYellow': '#d29922',
'brightBlue': '#58a6ff',
'brightMagenta': '#bc8cff',
'brightCyan': '#56d4dd',
'brightWhite': '#f0f6fc',
}
};
const ANSI16_ORDERED_KEYS = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
'brightBlack',
'brightRed',
'brightGreen',
'brightYellow',
'brightBlue',
'brightMagenta',
'brightCyan',
'brightWhite'
]
const cssSupports = (typeof CSS !== 'undefined' && typeof CSS.supports === 'function')
? CSS.supports.bind(CSS)
: null
const _probeEl = typeof document !== 'undefined' && document.createElement
? document.createElement('span')
: null
function isColor(value) {
if (typeof value !== 'string') return false
const s = value.trim()
if (!s) return false
// Prefer CSS.supports if available
if (cssSupports) return cssSupports('color', s)
// Fallback: assign to a style property and check whether it “sticks”
if (_probeEl) {
_probeEl.style.color = ''
_probeEl.style.color = s
return _probeEl.style.color !== ''
}
return false
}
function buildPaletteArray(paletteObj) {
// Canonical ANSI16 order as required by hterm/Blink.
return ANSI16_ORDERED_KEYS.map((k) => paletteObj[k])
}
function validateTheme(theme) {
if (!theme || typeof theme !== 'object') return 'theme is missing'
if (!theme.canvas || typeof theme.canvas !== 'object') return 'canvas is missing'
if (!theme.cursor || typeof theme.cursor !== 'object') return 'cursor is missing'
if (!theme.palette || typeof theme.palette !== 'object') return 'palette is missing'
if (!isColor(theme.canvas.background)) return 'canvas.background is invalid'
if (!isColor(theme.canvas.foreground)) return 'canvas.foreground is invalid'
if (!isColor(theme.cursor.background)) return 'cursor.background is invalid'
for (const k of ANSI16_ORDERED_KEYS) {
if (!isColor(theme.palette[k])) return `palette.${k} is invalid`
}
return null
}
function applyTheme(theme) {
const themeName =
theme && typeof theme.name === 'string' ? theme.name : 'Theme'
const term =
typeof t !== 'undefined'
? t
: typeof window !== 'undefined'
? window.t
: undefined
if (!term || !term.prefs_ || typeof term.prefs_.set !== 'function') {
console.error(`${themeName} error: terminal preferences API is unavailable.`)
return
}
const problem = validateTheme(theme)
if (problem) {
console.error(`${themeName} error: ${problem}; aborting.`)
return
}
const paletteArray = buildPaletteArray(theme.palette)
try {
term.prefs_.set('color-palette-overrides', paletteArray)
term.prefs_.set('background-color', theme.canvas.background)
term.prefs_.set('foreground-color', theme.canvas.foreground)
term.prefs_.set('cursor-color', theme.cursor.background)
} catch (err) {
console.error(`${themeName} error:`, err)
}
}
function setupTheme() {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
console.error('Theme error: matchMedia is unavailable; aborting.')
return
}
const mq = window.matchMedia('(prefers-color-scheme: dark)')
const apply = (e) => {
const isDark = e && typeof e.matches === 'boolean' ? e.matches : mq.matches
applyTheme(isDark ? DARK_THEME : LIGHT_THEME)
}
apply()
if (typeof mq.addEventListener === 'function') {
mq.addEventListener('change', apply)
} else if (typeof mq.addListener === 'function') {
mq.addListener(apply)
} else {
console.warn( 'Theme warning: neither addEventListener nor addListener'
+ ' are available; dynamic appearance switch will not be supported.'
)
}
}
setupTheme()
})()

Theme Builder Prompt

Objective

  • Produce a Blink Shell (iPadOS) JavaScript theme that:
    • Uses GitHub Primer “Primer light” and “Primer dark” colour tokens to define an ANSI16 palette.
    • Switches automatically between light and dark using prefers-color-scheme.
    • Maximises compatibility with Blink’s theme runtime and avoids runtime errors.
    • Keeps theme colour selections faithful to GitHub by selecting only exact colours from the official Primer token palette.

Mandatory research and citations

  • Use web browsing and cite primary sources for each of the following:
    • The latest official Blink theme format and runtime contract, including how the global t object is provided.
    • How t.prefs_.set(key, value) behaves and the complete set of supported preference keys in Blink themes.
    • The canonical structure, ordering, and expected data type for color-palette-overrides, and how it interacts with other appearance preferences.
      • Primary sources must include Blink documentation and Chromium/libapps (hterm preference manager) sources.
  • GitHub Primer sources:
    • Locate the current official token sources for:
      • “Primer light” (GitHub Light)
      • “Primer dark” (GitHub Dark, not dark_dimmed)
    • Extract and cite:
      • The complete colour token palette used as the candidate set (include neutrals and chromatic ramps).
      • Relevant canvas and foreground tokens used for background and text.
    • Record the exact version, tag, or commit used.

Hard constraints

  • JavaScript:
    • Output code must be ES2015+.
    • Avoid top-level await and non-standard APIs.
  • Blink compatibility:
    • Assume Blink provides a global t; do not introduce alternate entrypoints.
    • Add runtime checks for t, t.prefs_, and t.prefs_.set.
    • Failure handling (applies to all failures, including media-query and palette-resolution failures):
      • Emit a descriptive console.error(...) and abort theme application.
      • Do not partially apply preferences – do not call t.prefs_.set at all unless every required value has been fully resolved and validated.
      • Compute and validate all preferences up-front; only then apply them in one final pass.
  • Preferences:
    • Do not duplicate behaviour already configurable in Blink Settings.
    • Only set non-colour preferences when they are not user-configurable and are necessary for correctness; justify each such preference.
    • Do not set preferences in these categories:
      • Scrolling
      • Encoding
      • Sound
    • Do not set these keys:
      • find-result-color
      • find-result-selected-color

Required constant structure

  • Define both palettes as constants in exactly this shape:
    • const LIGHT_THEME = { ... }
    • const DARK_THEME = { ... }
  • Each must follow this structure and key spelling:
    • name – a human-readable identifier for the theme.
    • canvas – base drawing surface properties: foreground, background.
    • cursor – cursor properties: background (base RGB from Primer tokens; alpha may be tuned later).
    • palette – a collection of ANSI colours used by the theme:
      • black, red, green, yellow, blue, magenta, cyan, white
      • brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite

Canonical ANSI semantics

  • Use a single mandated canonical ANSI16 reference palette as immutable targets:
    • Use xterm’s default ANSI16 palette as the canonical target set; cite it.
    • List the 16 canonical hex targets used for distance calculations, mapped by key:
      • black, red, green, yellow, blue, magenta, cyan, white, brightBlack, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite
  • Preserve canonical ANSI slot semantics and canonical ordering (the key sequence above) when serialising to color-palette-overrides and when validating the grid.

Candidate pool rules

  • Theme colours must be selected from the extracted official Primer token palette (exact hex matches):

    • palette.* values must be exact hex values present in the extracted token set.
    • canvas.foreground and canvas.background must be exact hex values present in the extracted token set.
    • cursor.background base RGB must be an exact hex value present in the extracted token set; only its alpha may be tuned later by converting to rgba(r,g,b,a).
  • Do not modify, blend, lighten, darken, or otherwise compute new hex values for palette.*, canvas.*, or the cursor base RGB.

  • Deterministic Primer token family matching:

    • For ANSI palette slots, prefer Primer primitive ramp tokens (for example scale.<colour>.*) as candidates whenever available.
    • For canvas.*, prefer semantic canvas/text tokens when available, but still require that the resolved hex value is present in the extracted token set.
    • For cursor.background base RGB, prefer neutral families (for example neutral, gray, black, white) and constrain by low chroma (see thresholds below).
  • Duplicate hex values across tokens:

    • If multiple tokens share the same hex value, select the token name deterministically:
      • Prefer scale.* over semantic tokens.
      • If still tied, choose the first token by source order in the cited token file.
    • Record any ties and the chosen tie-break outcome in the audit trail.
  • Slot-scoped token family allowlists (by token name family), used to form candidate pools:

    • Neutrals:
      • black, brightBlack, white, brightWhiteneutral, gray, black, white
    • Chromatics:
      • red, brightRedred
      • green, brightGreengreen
      • yellow, brightYellowyellow, else gold (only if infeasible under hard constraints may expand to orange; must be recorded)
      • blue, brightBlueblue
      • magenta, brightMagentamagenta, else purple, else pink
      • cyan, brightCyancyan, else teal (fallback must be recorded)

Perceptual constraints and backoff

  • Convert sRGB hex values to OKLab and OKLCH for:

    • Distance comparisons (OKLab).
    • Hue constraints (OKLCH).
  • Default numeric chroma thresholds (may be tuned but must be stated and reported):

    • C_NEUTRAL_MAX = 0.03
    • C_CHROMATIC_MIN = 0.06
  • Integrate chroma thresholds into slot rules:

    • Neutrals must satisfy C ≤ C_NEUTRAL_MAX.
    • Chromatics must satisfy C ≥ C_CHROMATIC_MIN.
    • If a slot has no candidates after applying chroma thresholds, allow bounded chroma relaxation per slot:
      • Neutrals: increase C_NEUTRAL_MAX in steps of 0.01 up to 0.06.
      • Chromatics: decrease C_CHROMATIC_MIN in steps of 0.01 down to 0.03.
    • Record any chroma relaxation in the audit trail.
  • Hue constraints:

    • Neutrals:

      • Do not apply hue constraints to neutral slots.
      • Constrain neutrals by chroma and distance to the canonical neutral target.
    • Chromatics:

      • Apply a hue guard rail to all chromatic slots:
        • Default |Δh| ≤ 15° versus the canonical ANSI target hue for that slot.
        • If infeasible, allow bounded relaxation per slot in this order:
          • 15° then 20° then 25°
        • Record any relaxation in the audit trail.
    • Palette distinctness (applies to all 16 ANSI colours)

      • For each appearance, all palette.* entries must be mutually distinct: for every pair of different palette keys k1 != k2, require
        • distance_OKLab(palette[k1], palette[k2]) ≥ D_MIN.
      • Default: D_MIN = 0.06.
      • If infeasible under all hard constraints, allow bounded relaxation in steps and record it in the audit: 0.06 → 0.05 → 0.04.

Bright variants rule

  • Define “bright” as “more emphatic on the theme canvas background”:
    • For each hue pair (normal vs bright), the bright variant must not have lower contrast ratio as foreground on canvas.background than the normal variant.
    • Prefer strictly higher contrast when feasible without violating hard constraints or materially increasing distance; if equality is chosen, record it in the audit trail.
    • Keep hue aligned under the hue guard rail.

Dynamic switching

  • Implement automatic switching using window.matchMedia('(prefers-color-scheme: dark)').
  • If window.matchMedia is unavailable, emit a descriptive console.error and abort theme application (set nothing).
  • Use a standards-compliant change listener:
    • Prefer addEventListener('change', handler).
    • If a fallback is required, include it and justify it.
  • Apply the correct palette on initial load and on subsequent changes.

Legibility validation and optimisation

  • Define WCAG contrast ratio computed from sRGB hex values.

  • Hard constraints (must pass):

    • Each palette.* colour as foreground on canvas.background must have contrast ratio ≥ 4.5:1.
    • canvas.foreground on each palette.* colour used as background must have contrast ratio ≥ 4.5:1.
  • Full validation (nothing skipped):

    • Evaluate the complete 16×16 grid of foreground and background combinations using the canonical key ordering:
      • Rows iterate over palette keys in canonical order.
      • Columns iterate over palette keys in canonical order.
      • A cell at row key k_fg and column key k_bg uses:
        • Foreground: palette[k_fg]
        • Background: palette[k_bg]
      • Cell content is exactly gYw.
      • Only diagonal cells where k_fg == k_bg may be illegible.
    • Use a soft objective across all non-diagonal pairs:
      • Minimise a penalty defined as: Σ over all non-diagonal pairs of max(0, 3.0 - contrastRatio).
      • Report the minimum non-diagonal contrast ratio achieved, the worst offending pairs, and the total penalty value.
  • Optimisation objective (lexicographic):

    • Minimise, in order:
      • Number of hard-constraint violations.
      • Total hard-violation severity.
      • Maximum per-slot hue deviation (chromatic slots).
      • Maximum per-slot OKLab distance to canonical target.
      • Sum of OKLab distances across all slots.
      • Total soft-penalty across all non-diagonal pairs.
  • Cursor overlay model and optimisation:

    • After the legibility of canvas.* and palette.* properties is resolved, fix them as immutable.
    • Convert cursor.background from a Primer-token hex to rgba(r,g,b,a).
    • Adjust only alpha a in increments of 0.05 (bounded 0.0–1.0).
    • Define overlay as standard alpha compositing of cursor colour over the underlying background (CSS-style compositing).
    • Cursor visibility on the canvas background:
      • The effective cursor fill over canvas.background must be legible against canvas.background:
        • Composite cursor.background over canvas.background to get an effective cursor colour.
        • Require contrast ratio between effective cursor colour and canvas.background ≥ 3.0:1.
    • Text legibility under the cursor:
      • For each non-diagonal grid cell:
        • Composite cursor.background over the cell background palette[k_bg] to get an effective background under the cursor.
        • Re-check contrast for foreground palette[k_fg] against this effective background using the same hard/soft rules.
    • If no alpha value satisfies:
      • the established hard constraints for the required text cases, and
      • the cursor visibility constraint on canvas.background, Record failure in the output and set the cursor colour to an empty string (set nothing).

Output (strict)

  1. Two colour tables: - One for Light, one for Dark. - Each lists all palette keys in canonical order with:
    • ANSI semantic key
    • canonical target hex
    • chosen Primer token name
    • chosen hex
    • OKLab distance to canonical
    • OKLCH hue delta (or N/A for neutrals)
    • Contrast on canvas.background
    • Contrast of canvas.foreground on this colour as background
    • Worst non-diagonal contrast involving this slot and count of pairs below 3.0:1
    • Notes on any hue/chroma relaxation or token-name tie-breaks used
  2. Two rendered artefacts of the resolved 16×16 grid (Light and Dark) showing gYw with actual foreground/background colours – provided as PNG images and as self-contained SVG embedded in a code block.
    • The grid in the images must be set on the coresponding background.
  3. One self-contained HTML file (in a code block) that renders both 16×1 6 tables using inline CSS, so it can be saved and opened locally.
  4. One self-contained theme file: - Provide inside a single JavaScript code block. - Must include:
    • LIGHT_THEME and DARK_THEME constants in the required structure.
    • The dynamic switching logic.
    • Failure handling exactly as specified in Hard constraints (descriptive console.error, abort, no partial application).
    • A deterministic mapping from:
      • canvas.foregroundforeground-color
      • canvas.backgroundbackground-color
      • cursor.backgroundcursor-color
      • palette (keys in canonical order) → color-palette-overrides
  5. Concise justification of every non-colour preference set: - Explain why it is necessary and why it does not duplicate Blink Settings behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment