From 07485222458185ea71004266619223cb22270b3c Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 27 Jul 2026 21:35:09 -0700 Subject: [PATCH] feat(zed): add Australis Dark theme for Zed Adds a Zed theme generated from the canonical palette in the root README, alongside the existing wezterm and zellij configurations. The 16 terminal colors carry over unchanged from wezterm/config.lua. Syntax highlighting follows the spec's stated preference order (blue, cyan, green) with the Dawn accents reserved for semantic signals and the rare token roles. Zed addresses UI surfaces the 16-color terminal palette does not define. Two neutrals are derived for chrome (#2a2e39, L19) and elevated surfaces (#313440, L22), both interpolated in CIELAB along the palette's own neutral ramp so they sit on the ramp the spec defines. ANSI dim variants are mixed 45% toward the background in LAB to preserve hue. Overlays use an alpha channel over palette colors rather than new opaque values. The theme is generated by build-theme.py, which validates output against the live Zed theme schema (v0.2.0) and fails if any of its 142 style keys is missing. --- zed/README.md | 79 ++++++++ zed/australis-dark.json | 426 ++++++++++++++++++++++++++++++++++++++ zed/build-theme.py | 440 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 945 insertions(+) create mode 100644 zed/README.md create mode 100644 zed/australis-dark.json create mode 100644 zed/build-theme.py diff --git a/zed/README.md b/zed/README.md new file mode 100644 index 0000000..36abfd3 --- /dev/null +++ b/zed/README.md @@ -0,0 +1,79 @@ +## Zed Australis Dark + +### Installation Instructions + +Copy `australis-dark.json` into your Zed themes directory: + +```sh +mkdir -p ~/.config/zed/themes +cp australis-dark.json ~/.config/zed/themes/ +``` + +Zed loads themes from that directory without a restart. Select +**Australis Dark** from `theme selector: toggle` (`cmd-k cmd-t`), or set it +in `settings.json`: + +```json +"theme": { + "mode": "dark", + "dark": "Australis Dark" +} +``` + +### Palette usage + +The 16 terminal colors are carried over unchanged from +[`wezterm/config.lua`](../wezterm/config.lua). Syntax highlighting follows the +spec's stated preference order — blue, then cyan, then green — with the Dawn +accents kept rare: + +| Role | Color | +| :-------------------------------------- | :------------------------------- | +| Keywords, tags | Blue `#6388d8` | +| Functions, methods | Bright cyan `#42dcd1` | +| Types, constructors, enums | Cyan `#00b1a8` | +| Strings | Green `#16b866` | +| String escapes, regex | Bright green `#51e08a` | +| Numbers, booleans, constants | Bright blue `#a4c4ff` | +| Comments | Dark 50 `#6e7882` | +| Operators, punctuation | Dark 60 `#86929d` | +| Decorators, `self`, preprocessor | Magenta `#9d78ff` / `#d8adff` | + +Dawn accents otherwise appear only where the editor demands a semantic +signal: red for errors and deletions, yellow for warnings and modifications, +bright yellow for search matches (kept distinct from the blue selection and +the cyan bracket match). + +### Derived colors + +Zed addresses UI surfaces the 16-color terminal palette does not define. Two +neutrals are therefore derived, both interpolated in CIELAB **along the +palette's own neutral ramp** so they land on the ramp the spec defines rather +than introducing a foreign hue: + +| Derived | Hex | LAB L\* | Ramp position | Used for | +| :------- | :-------- | :------ | :--------------- | :------------------------------ | +| Chrome | `#2a2e39` | 19 | 40% of L15 → L25 | Tab bar, panels, status bar | +| Elevated | `#313440` | 22 | 70% of L15 → L25 | Popovers, modals, context menus | + +The editor canvas itself remains the palette's black `#222531`, with the +active tab fusing into it. ANSI `dim_*` variants, which Zed requires and the +palette does not specify, are each mixed 45% toward the background in LAB so +hue is preserved. + +Overlay colors (selections, hover states, status backgrounds) use an alpha +channel over the palette color rather than a new opaque value, keeping the +underlying color recognizable. + +### Regenerating the theme + +`australis-dark.json` is generated, not hand-maintained. Edit +`build-theme.py` and re-run it: + +```sh +python3 build-theme.py australis-dark.json +``` + +The script requires no third-party packages. It fetches the live Zed theme +schema and fails if any of the 142 style keys it defines is missing, or if a +key outside the schema is emitted; the check is skipped when offline. diff --git a/zed/australis-dark.json b/zed/australis-dark.json new file mode 100644 index 0000000..a37f500 --- /dev/null +++ b/zed/australis-dark.json @@ -0,0 +1,426 @@ +{ + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", + "name": "Australis", + "author": "Vuong Hoang", + "themes": [ + { + "name": "Australis Dark", + "appearance": "dark", + "style": { + "background.appearance": "opaque", + "background": "#2a2e39", + "surface.background": "#2a2e39", + "elevated_surface.background": "#313440", + "panel.background": "#2a2e39", + "status_bar.background": "#2a2e39", + "title_bar.background": "#2a2e39", + "title_bar.inactive_background": "#2a2e39", + "toolbar.background": "#222531", + "tab_bar.background": "#2a2e39", + "tab.active_background": "#222531", + "tab.inactive_background": "#2a2e39", + "drop_target.background": "#6388d833", + "border": "#414751", + "border.variant": "#373b46", + "border.focused": "#6388d8", + "border.selected": "#6388d8", + "border.transparent": "#00000000", + "border.disabled": "#373b46", + "pane.focused_border": "#6388d8", + "pane_group.border": "#373b46", + "panel.focused_border": "#6388d8", + "panel.indent_guide": "#373b46", + "panel.indent_guide_active": "#565f69", + "panel.indent_guide_hover": "#414751", + "element.background": "#2a2e39", + "element.hover": "#373b4699", + "element.active": "#414751", + "element.selected": "#6388d833", + "element.disabled": "#373b4666", + "ghost_element.background": "#00000000", + "ghost_element.hover": "#373b4680", + "ghost_element.active": "#41475199", + "ghost_element.selected": "#6388d826", + "ghost_element.disabled": "#00000000", + "text": "#a9bcc3", + "text.muted": "#86929d", + "text.placeholder": "#6e7882", + "text.disabled": "#565f69", + "text.accent": "#a4c4ff", + "link_text.hover": "#a4c4ff", + "icon": "#a9bcc3", + "icon.muted": "#86929d", + "icon.disabled": "#565f69", + "icon.placeholder": "#6e7882", + "icon.accent": "#42dcd1", + "editor.background": "#222531", + "editor.foreground": "#a9bcc3", + "editor.gutter.background": "#222531", + "editor.subheader.background": "#2a2e39", + "editor.active_line.background": "#373b4659", + "editor.highlighted_line.background": "#373b4680", + "editor.line_number": "#565f69", + "editor.active_line_number": "#cce7ec", + "editor.invisible": "#565f69", + "editor.wrap_guide": "#373b4680", + "editor.active_wrap_guide": "#565f69", + "editor.indent_guide": "#373b46", + "editor.indent_guide_active": "#565f69", + "editor.document_highlight.read_background": "#6388d826", + "editor.document_highlight.write_background": "#42dcd133", + "editor.document_highlight.bracket_background": "#6388d838", + "search.match_background": "#ffe14e38", + "scrollbar.thumb.background": "#565f6966", + "scrollbar.thumb.hover_background": "#6e788299", + "scrollbar.thumb.border": "#00000000", + "scrollbar.track.background": "#00000000", + "scrollbar.track.border": "#00000000", + "error": "#ff491a", + "error.background": "#ff491a1f", + "error.border": "#ff491a66", + "warning": "#e1c631", + "warning.background": "#e1c6311f", + "warning.border": "#e1c63166", + "info": "#6388d8", + "info.background": "#6388d81f", + "info.border": "#6388d866", + "success": "#16b866", + "success.background": "#16b8661f", + "success.border": "#16b86666", + "hint": "#00b1a8", + "hint.background": "#00b1a81f", + "hint.border": "#00b1a866", + "predictive": "#565f69", + "predictive.background": "#565f691f", + "predictive.border": "#565f6966", + "created": "#16b866", + "created.background": "#16b8661f", + "created.border": "#16b86666", + "modified": "#e1c631", + "modified.background": "#e1c6311f", + "modified.border": "#e1c63166", + "deleted": "#ff491a", + "deleted.background": "#ff491a1f", + "deleted.border": "#ff491a66", + "renamed": "#6388d8", + "renamed.background": "#6388d81f", + "renamed.border": "#6388d866", + "conflict": "#ff854f", + "conflict.background": "#ff854f1f", + "conflict.border": "#ff854f66", + "ignored": "#6e7882", + "ignored.background": "#6e78821f", + "ignored.border": "#6e788266", + "hidden": "#6e7882", + "hidden.background": "#6e78821f", + "hidden.border": "#6e788266", + "unreachable": "#6e7882", + "unreachable.background": "#6e78821f", + "unreachable.border": "#6e788266", + "terminal.background": "#222531", + "terminal.foreground": "#a9bcc3", + "terminal.bright_foreground": "#cce7ec", + "terminal.dim_foreground": "#6e7882", + "terminal.ansi.background": "#222531", + "terminal.ansi.black": "#222531", + "terminal.ansi.red": "#ff491a", + "terminal.ansi.green": "#16b866", + "terminal.ansi.yellow": "#e1c631", + "terminal.ansi.blue": "#6388d8", + "terminal.ansi.magenta": "#9d78ff", + "terminal.ansi.cyan": "#00b1a8", + "terminal.ansi.white": "#a9bcc3", + "terminal.ansi.bright_black": "#373b46", + "terminal.ansi.bright_red": "#ff854f", + "terminal.ansi.bright_green": "#51e08a", + "terminal.ansi.bright_yellow": "#ffe14e", + "terminal.ansi.bright_blue": "#a4c4ff", + "terminal.ansi.bright_magenta": "#d8adff", + "terminal.ansi.bright_cyan": "#42dcd1", + "terminal.ansi.bright_white": "#cce7ec", + "terminal.ansi.dim_black": "#2c303b", + "terminal.ansi.dim_red": "#9a3e2b", + "terminal.ansi.dim_green": "#2e724f", + "terminal.ansi.dim_yellow": "#88783a", + "terminal.ansi.dim_blue": "#465988", + "terminal.ansi.dim_magenta": "#63519c", + "terminal.ansi.dim_cyan": "#2a6e70", + "terminal.ansi.dim_white": "#6e7882", + "accents": [ + "#6388d8", + "#00b1a8", + "#16b866", + "#9d78ff", + "#e1c631", + "#42dcd1", + "#a4c4ff", + "#51e08a" + ], + "players": [ + { + "cursor": "#6388d8", + "background": "#6388d8", + "selection": "#6388d83d" + }, + { + "cursor": "#16b866", + "background": "#16b866", + "selection": "#16b8663d" + }, + { + "cursor": "#9d78ff", + "background": "#9d78ff", + "selection": "#9d78ff3d" + }, + { + "cursor": "#00b1a8", + "background": "#00b1a8", + "selection": "#00b1a83d" + }, + { + "cursor": "#ff854f", + "background": "#ff854f", + "selection": "#ff854f3d" + }, + { + "cursor": "#e1c631", + "background": "#e1c631", + "selection": "#e1c6313d" + }, + { + "cursor": "#42dcd1", + "background": "#42dcd1", + "selection": "#42dcd13d" + }, + { + "cursor": "#d8adff", + "background": "#d8adff", + "selection": "#d8adff3d" + } + ], + "syntax": { + "comment": { + "color": "#6e7882", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "#86929d", + "font_style": "italic", + "font_weight": null + }, + "operator": { + "color": "#86929d", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "#86929d", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "#86929d", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "#86929d", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "#6388d8", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "#d8adff", + "font_style": null, + "font_weight": null + }, + "variable": { + "color": "#a9bcc3", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "#9daeb6", + "font_style": null, + "font_weight": null + }, + "primary": { + "color": "#a9bcc3", + "font_style": null, + "font_weight": null + }, + "hint": { + "color": "#6e7882", + "font_style": "italic", + "font_weight": null + }, + "predictive": { + "color": "#565f69", + "font_style": "italic", + "font_weight": null + }, + "keyword": { + "color": "#6388d8", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "#6388d8", + "font_style": null, + "font_weight": null + }, + "link_text": { + "color": "#6388d8", + "font_style": "italic", + "font_weight": null + }, + "label": { + "color": "#a4c4ff", + "font_style": null, + "font_weight": null + }, + "title": { + "color": "#a4c4ff", + "font_style": null, + "font_weight": 700 + }, + "function": { + "color": "#42dcd1", + "font_style": null, + "font_weight": null + }, + "function.definition": { + "color": "#42dcd1", + "font_style": null, + "font_weight": null + }, + "function.method": { + "color": "#42dcd1", + "font_style": null, + "font_weight": null + }, + "function.special.definition": { + "color": "#42dcd1", + "font_style": null, + "font_weight": null + }, + "type": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "type.interface": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "type.super": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "enum": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "variant": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "link_uri": { + "color": "#00b1a8", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "#42dcd1", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "#42dcd1", + "font_style": null, + "font_weight": 700 + }, + "string": { + "color": "#16b866", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "#51e08a", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "#51e08a", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "#51e08a", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "#51e08a", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "#16b866", + "font_style": null, + "font_weight": null + }, + "number": { + "color": "#a4c4ff", + "font_style": null, + "font_weight": null + }, + "boolean": { + "color": "#a4c4ff", + "font_style": null, + "font_weight": null + }, + "constant": { + "color": "#a4c4ff", + "font_style": null, + "font_weight": null + }, + "attribute": { + "color": "#d8adff", + "font_style": null, + "font_weight": null + }, + "variable.special": { + "color": "#9d78ff", + "font_style": "italic", + "font_weight": null + }, + "preproc": { + "color": "#9d78ff", + "font_style": null, + "font_weight": null + }, + "embedded": { + "color": "#9d78ff", + "font_style": null, + "font_weight": null + } + } + } + } + ] +} diff --git a/zed/build-theme.py b/zed/build-theme.py new file mode 100644 index 0000000..56cd54c --- /dev/null +++ b/zed/build-theme.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +"""Generate the Australis Dark theme for Zed from the canonical palette. + +Palette source: ../README.md ("All Colors and Hex Codes") +Target schema: https://zed.dev/schema/themes/v0.2.0.json + +Usage: + python3 build-theme.py [output.json] # defaults to australis-dark.json + +Two classes of color appear in the output: + + 1. PALETTE - verbatim from the repo README. Never altered. + 2. DERIVED - computed from the palette, and only where Zed requires a step + the 16-color terminal palette does not define (UI elevation + surfaces, dim ANSI variants). Every derived color is + interpolated in CIELAB *along the palette's own neutral ramp*, + so it lands on the ramp the spec defines rather than + introducing a foreign hue. Overlays use an alpha channel so + the underlying palette color stays recognizable. + +The script self-checks against the live Zed schema when the network is +available: every style key the schema defines must be present, and no key +outside the schema may be emitted. +""" + +import json +import sys + +# --------------------------------------------------------------------------- +# CIELAB (D65) conversion - used only to derive on-ramp neutrals +# --------------------------------------------------------------------------- + +WHITEPOINT = (0.95047, 1.0, 1.08883) + + +def _hex_to_rgb(h): + h = h.lstrip("#") + return tuple(int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)) + + +def _rgb_to_hex(rgb): + return "#" + "".join("%02x" % max(0, min(255, round(c * 255))) for c in rgb) + + +def _linearize(c): + return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 + + +def _delinearize(c): + return c * 12.92 if c <= 0.0031308 else 1.055 * (c ** (1 / 2.4)) - 0.055 + + +def _f(t): + return t ** (1 / 3) if t > 216 / 24389 else (841 / 108) * t + 4 / 29 + + +def _f_inv(t): + return t ** 3 if t ** 3 > 216 / 24389 else (t - 4 / 29) * 108 / 841 + + +def hex_to_lab(h): + r, g, b = (_linearize(c) for c in _hex_to_rgb(h)) + xyz = (0.4124564 * r + 0.3575761 * g + 0.1804375 * b, + 0.2126729 * r + 0.7151522 * g + 0.0721750 * b, + 0.0193339 * r + 0.1191920 * g + 0.9503041 * b) + fx, fy, fz = (_f(v / w) for v, w in zip(xyz, WHITEPOINT)) + return (116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)) + + +def lab_to_hex(lab): + L, a_, b_ = lab + fy = (L + 16) / 116 + x, y, z = (_f_inv(fy + a_ / 500) * WHITEPOINT[0], + _f_inv(fy) * WHITEPOINT[1], + _f_inv(fy - b_ / 200) * WHITEPOINT[2]) + return _rgb_to_hex(tuple(_delinearize(v) for v in ( + 3.2404542 * x - 1.5371385 * y - 0.4985314 * z, + -0.9692660 * x + 1.8760108 * y + 0.0415560 * z, + 0.0556434 * x - 0.2040259 * y + 1.0572252 * z))) + + +def lab_mix(color_a, color_b, t): + """Interpolate in CIELAB. t=0 -> color_a, t=1 -> color_b.""" + la, lb = hex_to_lab(color_a), hex_to_lab(color_b) + return lab_to_hex(tuple(x + (y - x) * t for x, y in zip(la, lb))) + + +# --------------------------------------------------------------------------- +# PALETTE - verbatim from the repo README +# --------------------------------------------------------------------------- + +# Ice / Sea - the neutral ramp, indexed by CIELAB lightness +L15 = "#222531" # black - background +L25 = "#373b46" # bright black +L30 = "#414751" # dark 30 +L40 = "#565f69" # dark 40 +L50 = "#6e7882" # dark 50 +L60 = "#86929d" # dark 60 +L70 = "#9daeb6" # bright 70 +L75 = "#a9bcc3" # white - foreground +L80 = "#b3cbcf" # bright 80 +L90 = "#cce7ec" # bright white - highlight + +# Aurora - primary colors (used generously; preference order blue > cyan > green) +BLUE = "#6388d8" +BLUE_BR = "#a4c4ff" +CYAN = "#00b1a8" +CYAN_BR = "#42dcd1" +GREEN = "#16b866" +GREEN_BR = "#51e08a" + +# Dawn - accent colors (used sparingly) +RED = "#ff491a" +RED_BR = "#ff854f" +YELLOW = "#e1c631" +YELLOW_BR = "#ffe14e" +MAGENTA = "#9d78ff" +MAGENTA_BR = "#d8adff" + +# --------------------------------------------------------------------------- +# DERIVED - sampled from the palette's own neutral ramp +# --------------------------------------------------------------------------- + +# The palette's darkest step (L15) is the editor canvas. Zed additionally needs +# chrome and floating-surface elevations between L15 and L25; sampling the +# L15 -> L25 segment keeps both on-ramp. +CHROME = lab_mix(L15, L25, 0.40) # ~L19 - tab bar, panels, status bar +ELEVATED = lab_mix(L15, L25, 0.70) # ~L22 - popovers, modals, context menus + +TRANSPARENT = "#00000000" + + +def dim(color): + """ANSI dim variant: 45% toward the background in LAB, preserving hue.""" + return lab_mix(color, L15, 0.45) + + +def alpha(color, value): + """Attach an 8-bit alpha channel to a palette color (Zed takes #RRGGBBAA).""" + return "%s%02x" % (color, value) + + +# --------------------------------------------------------------------------- +# Syntax +# --------------------------------------------------------------------------- + + +def hl(color, style=None, weight=None): + return {"color": color, "font_style": style, "font_weight": weight} + + +# Aurora carries the bulk of the syntax volume; Dawn accents stay rare - +# only variable.special, decorators and preprocessor tokens. +SYNTAX = { + # Neutrals - structure that should recede + "comment": hl(L50, style="italic"), + "comment.doc": hl(L60, style="italic"), + "operator": hl(L60), + "punctuation": hl(L60), + "punctuation.bracket": hl(L60), + "punctuation.delimiter": hl(L60), + "punctuation.list_marker": hl(BLUE), + "punctuation.special": hl(MAGENTA_BR), + "variable": hl(L75), + "property": hl(L70), + "primary": hl(L75), + "hint": hl(L50, style="italic"), + "predictive": hl(L40, style="italic"), + + # Aurora - blue: keywords and tags (frequent tokens, sits back at L57) + "keyword": hl(BLUE), + "tag": hl(BLUE), + "link_text": hl(BLUE, style="italic"), + "label": hl(BLUE_BR), + "title": hl(BLUE_BR, weight=700), + + # Aurora - cyan: the callable/type layer (bright = the visual anchors) + "function": hl(CYAN_BR), + "function.definition": hl(CYAN_BR), + "function.method": hl(CYAN_BR), + "function.special.definition": hl(CYAN_BR), + "type": hl(CYAN), + "type.interface": hl(CYAN), + "type.super": hl(CYAN), + "constructor": hl(CYAN), + "enum": hl(CYAN), + "variant": hl(CYAN), + "link_uri": hl(CYAN), + "emphasis": hl(CYAN_BR, style="italic"), + "emphasis.strong": hl(CYAN_BR, weight=700), + + # Aurora - green: literal data + "string": hl(GREEN), + "string.escape": hl(GREEN_BR), + "string.regex": hl(GREEN_BR), + "string.special": hl(GREEN_BR), + "string.special.symbol": hl(GREEN_BR), + "text.literal": hl(GREEN), + + # Bright blue: scalar literals - distinct from strings, still Aurora + "number": hl(BLUE_BR), + "boolean": hl(BLUE_BR), + "constant": hl(BLUE_BR), + + # Dawn - sparing use only + "attribute": hl(MAGENTA_BR), + "variable.special": hl(MAGENTA, style="italic"), + "preproc": hl(MAGENTA), + "embedded": hl(MAGENTA), +} + +# Collaboration cursors - Aurora first, Dawn for the tail +PLAYER_COLORS = [BLUE, GREEN, MAGENTA, CYAN, RED_BR, YELLOW, CYAN_BR, MAGENTA_BR] +PLAYERS = [{"cursor": c, "background": c, "selection": alpha(c, 0x3D)} + for c in PLAYER_COLORS] + +# --------------------------------------------------------------------------- +# Full style map +# --------------------------------------------------------------------------- + +STYLE = { + # --- Base surfaces ----------------------------------------------------- + "background.appearance": "opaque", + "background": CHROME, + "surface.background": CHROME, + "elevated_surface.background": ELEVATED, + "panel.background": CHROME, + "status_bar.background": CHROME, + "title_bar.background": CHROME, + "title_bar.inactive_background": CHROME, + "toolbar.background": L15, + "tab_bar.background": CHROME, + "tab.active_background": L15, # active tab fuses with the canvas + "tab.inactive_background": CHROME, + "drop_target.background": alpha(BLUE, 0x33), + + # --- Borders ----------------------------------------------------------- + "border": L30, + "border.variant": L25, + "border.focused": BLUE, + "border.selected": BLUE, + "border.transparent": TRANSPARENT, + "border.disabled": L25, + "pane.focused_border": BLUE, + "pane_group.border": L25, + "panel.focused_border": BLUE, + "panel.indent_guide": L25, + "panel.indent_guide_active": L40, + "panel.indent_guide_hover": L30, + + # --- Interactive elements ---------------------------------------------- + "element.background": CHROME, + "element.hover": alpha(L25, 0x99), + "element.active": L30, + "element.selected": alpha(BLUE, 0x33), + "element.disabled": alpha(L25, 0x66), + "ghost_element.background": TRANSPARENT, + "ghost_element.hover": alpha(L25, 0x80), + "ghost_element.active": alpha(L30, 0x99), + "ghost_element.selected": alpha(BLUE, 0x26), + "ghost_element.disabled": TRANSPARENT, + + # --- Text -------------------------------------------------------------- + "text": L75, + "text.muted": L60, + "text.placeholder": L50, + "text.disabled": L40, + "text.accent": BLUE_BR, + "link_text.hover": BLUE_BR, + + # --- Icons ------------------------------------------------------------- + "icon": L75, + "icon.muted": L60, + "icon.disabled": L40, + "icon.placeholder": L50, + "icon.accent": CYAN_BR, + + # --- Editor ------------------------------------------------------------ + "editor.background": L15, + "editor.foreground": L75, + "editor.gutter.background": L15, + "editor.subheader.background": CHROME, + "editor.active_line.background": alpha(L25, 0x59), + "editor.highlighted_line.background": alpha(L25, 0x80), + "editor.line_number": L40, + "editor.active_line_number": L90, + "editor.invisible": L40, + "editor.wrap_guide": alpha(L25, 0x80), + "editor.active_wrap_guide": L40, + "editor.indent_guide": L25, + "editor.indent_guide_active": L40, + "editor.document_highlight.read_background": alpha(BLUE, 0x26), + "editor.document_highlight.write_background": alpha(CYAN_BR, 0x33), + "editor.document_highlight.bracket_background": alpha(BLUE, 0x38), + # Search match uses the bright-yellow highlight accent so it never reads + # as a selection (blue) or a bracket match (cyan). + "search.match_background": alpha(YELLOW_BR, 0x38), + + # --- Scrollbar --------------------------------------------------------- + "scrollbar.thumb.background": alpha(L40, 0x66), + "scrollbar.thumb.hover_background": alpha(L50, 0x99), + "scrollbar.thumb.border": TRANSPARENT, + "scrollbar.track.background": TRANSPARENT, + "scrollbar.track.border": TRANSPARENT, + + # --- Status ------------------------------------------------------------ + "error": RED, + "error.background": alpha(RED, 0x1F), + "error.border": alpha(RED, 0x66), + "warning": YELLOW, + "warning.background": alpha(YELLOW, 0x1F), + "warning.border": alpha(YELLOW, 0x66), + "info": BLUE, + "info.background": alpha(BLUE, 0x1F), + "info.border": alpha(BLUE, 0x66), + "success": GREEN, + "success.background": alpha(GREEN, 0x1F), + "success.border": alpha(GREEN, 0x66), + "hint": CYAN, + "hint.background": alpha(CYAN, 0x1F), + "hint.border": alpha(CYAN, 0x66), + "predictive": L40, + "predictive.background": alpha(L40, 0x1F), + "predictive.border": alpha(L40, 0x66), + + # --- Version control --------------------------------------------------- + "created": GREEN, + "created.background": alpha(GREEN, 0x1F), + "created.border": alpha(GREEN, 0x66), + "modified": YELLOW, + "modified.background": alpha(YELLOW, 0x1F), + "modified.border": alpha(YELLOW, 0x66), + "deleted": RED, + "deleted.background": alpha(RED, 0x1F), + "deleted.border": alpha(RED, 0x66), + "renamed": BLUE, + "renamed.background": alpha(BLUE, 0x1F), + "renamed.border": alpha(BLUE, 0x66), + "conflict": RED_BR, + "conflict.background": alpha(RED_BR, 0x1F), + "conflict.border": alpha(RED_BR, 0x66), + "ignored": L50, + "ignored.background": alpha(L50, 0x1F), + "ignored.border": alpha(L50, 0x66), + "hidden": L50, + "hidden.background": alpha(L50, 0x1F), + "hidden.border": alpha(L50, 0x66), + "unreachable": L50, + "unreachable.background": alpha(L50, 0x1F), + "unreachable.border": alpha(L50, 0x66), + + # --- Terminal: the 16 ANSI colors, matching wezterm/config.lua ---------- + "terminal.background": L15, + "terminal.foreground": L75, + "terminal.bright_foreground": L90, + "terminal.dim_foreground": L50, + "terminal.ansi.background": L15, + "terminal.ansi.black": L15, + "terminal.ansi.red": RED, + "terminal.ansi.green": GREEN, + "terminal.ansi.yellow": YELLOW, + "terminal.ansi.blue": BLUE, + "terminal.ansi.magenta": MAGENTA, + "terminal.ansi.cyan": CYAN, + "terminal.ansi.white": L75, + "terminal.ansi.bright_black": L25, + "terminal.ansi.bright_red": RED_BR, + "terminal.ansi.bright_green": GREEN_BR, + "terminal.ansi.bright_yellow": YELLOW_BR, + "terminal.ansi.bright_blue": BLUE_BR, + "terminal.ansi.bright_magenta": MAGENTA_BR, + "terminal.ansi.bright_cyan": CYAN_BR, + "terminal.ansi.bright_white": L90, + "terminal.ansi.dim_black": lab_mix(L15, L25, 0.5), + "terminal.ansi.dim_red": dim(RED), + "terminal.ansi.dim_green": dim(GREEN), + "terminal.ansi.dim_yellow": dim(YELLOW), + "terminal.ansi.dim_blue": dim(BLUE), + "terminal.ansi.dim_magenta": dim(MAGENTA), + "terminal.ansi.dim_cyan": dim(CYAN), + "terminal.ansi.dim_white": L50, + + # --- Rotating accents (rainbow indent guides, etc.) -------------------- + "accents": [BLUE, CYAN, GREEN, MAGENTA, YELLOW, CYAN_BR, BLUE_BR, GREEN_BR], + + "players": PLAYERS, + "syntax": SYNTAX, +} + +THEME = { + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", + "name": "Australis", + "author": "Vuong Hoang", + "themes": [ + { + "name": "Australis Dark", + "appearance": "dark", + "style": STYLE, + } + ], +} + +SCHEMA_URL = "https://zed.dev/schema/themes/v0.2.0.json" + + +def check_against_schema(): + """Assert full coverage of the schema's style keys. Skipped when offline.""" + import urllib.request + + # zed.dev rejects urllib's default user-agent with a 403. + request = urllib.request.Request(SCHEMA_URL, headers={"User-Agent": "curl/8"}) + try: + with urllib.request.urlopen(request, timeout=10) as response: + schema = json.load(response) + except Exception as exc: # offline, or zed.dev unreachable + print("schema check skipped (%s)" % exc, file=sys.stderr) + return + + expected = set(schema["definitions"]["ThemeStyleContent"]["properties"]) + missing, extra = expected - set(STYLE), set(STYLE) - expected + if missing or extra: + print("MISSING (%d): %s" % (len(missing), sorted(missing)), file=sys.stderr) + print("EXTRA (%d): %s" % (len(extra), sorted(extra)), file=sys.stderr) + sys.exit(1) + print("all %d schema style keys present" % len(expected), file=sys.stderr) + + +def main(): + check_against_schema() + out_path = sys.argv[1] if len(sys.argv) > 1 else "australis-dark.json" + with open(out_path, "w") as handle: + json.dump(THEME, handle, indent=2) + handle.write("\n") + print("wrote %s (chrome=%s L%.0f, elevated=%s L%.0f)" % ( + out_path, CHROME, hex_to_lab(CHROME)[0], + ELEVATED, hex_to_lab(ELEVATED)[0]), file=sys.stderr) + + +if __name__ == "__main__": + main()