Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .opencode/themes/dreamcoder.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
},
"theme": {
"background": "none",
"backgroundPanel": "#fff7ea",
"backgroundElement": "#e3d2bc",
"backgroundPanel": "none",
"backgroundElement": "#e6d7c4",
"backgroundHover": "#decbb1",
"backgroundSelected": "#decbb1",
"textSelected": "#17120d",
"backgroundCode": "#fff7ea",
"backgroundCode": "#e6d7c4",
"backgroundSearch": "#a1895c",
"backgroundLine": "#e6d7c4",
"backgroundAssistant": "#d7d7ce",
Expand Down Expand Up @@ -79,12 +79,12 @@
"markdownImage": "#7d3e64",
"markdownImageText": "#17120d",
"markdownCodeBlock": "#17120d",
"markdownCodeBlockBg": "#fff7ea",
"markdownCodeBlockBg": "#e6d7c4",
"markdownInlineCodeBg": "#e7e1d2",
"syntaxComment": "#725e4c",
"syntaxComment": "#5d4d3e",
"syntaxKeyword": "#824f16",
"syntaxFunction": "#a7471c",
"syntaxMethod": "#a7471c",
"syntaxFunction": "#893a17",
"syntaxMethod": "#893a17",
"syntaxVariable": "#352e22",
"syntaxParameter": "#9e441b",
"syntaxProperty": "#0d4864",
Expand All @@ -97,15 +97,15 @@
"syntaxClass": "#224b59",
"syntaxInterface": "#0d4a68",
"syntaxEnum": "#134d5e",
"syntaxOperator": "#a7471c",
"syntaxOperator": "#893a17",
"syntaxPunctuation": "#352e22",
"syntaxTag": "#824f16",
"syntaxAttribute": "#0d4864",
"syntaxRegexp": "#7f3a52",
"syntaxEscape": "#654300",
"syntaxNamespace": "#0d4a68",
"syntaxModule": "#0d4a68",
"syntaxDecorator": "#a7471c",
"syntaxDecorator": "#893a17",
"syntaxBuiltin": "#7d3e64",
"syntaxSpecial": "#654300",
"syntaxTodo": "#5c3d02",
Expand Down
9 changes: 9 additions & 0 deletions src/dreamcoder_theme/renderers_opencode.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ def opencode_content(c: dict[str, str], transparent_background: bool = False) ->
muted = c["muted"]
subtle = guard(c["subtle"], c["bg"], mode_name, minimum=5.5)
placeholder = c["comment"]
if transparent_background:
# OpenCode shares the terminal canvas; only functional blocks get a fill.
# Shared syntax tokens stay unchanged; rendered Light roles guard below.
panel_bg = "none"
element_bg = code_bg = line_bg = c["bg_soft"]
for surface in (c["bg"], c["bg_soft"]):
muted = guard(muted, surface, mode_name)
subtle = guard(subtle, surface, mode_name)
placeholder = guard(placeholder, surface, mode_name)
added, removed = c["sage"], c["error"]
hunk, fold, markdown_code = c["lavender"], c["comment"], c["sage"]
if mode_name == "light":
Expand Down
112 changes: 112 additions & 0 deletions tests/test_opencode_visual_coherence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""OpenCode-only surface hierarchy and readable secondary text."""

import json
from pathlib import Path

import pytest

from dreamcoder_theme.palette import contrast, detect_mode, guard, mix
from dreamcoder_theme.renderers_opencode import opencode_content

TOKENS_FILE = Path(__file__).resolve().parents[1] / "DreamcoderThemes/dreamcoder/tokens.json"
VARIANTS = json.loads(TOKENS_FILE.read_text())["modes"]


@pytest.fixture(params=list(VARIANTS))
def palette(request):
return VARIANTS[request.param].copy()


def test_transparent_panel_inherits_terminal_canvas(palette):
theme = json.loads(opencode_content(palette, transparent_background=True))["theme"]
assert theme["background"] == "none"
assert theme["backgroundPanel"] == "none"


@pytest.mark.parametrize(
"role",
["backgroundElement", "backgroundCode", "backgroundLine", "markdownCodeBlockBg", "diffFoldBg"],
)
def test_transparent_functional_surfaces_use_canonical_soft_background(palette, role):
theme = json.loads(opencode_content(palette, transparent_background=True))["theme"]
assert theme[role] == palette["bg_soft"]


@pytest.mark.parametrize("role", ["text", "textMuted", "textSubtle", "textPlaceholder"])
@pytest.mark.parametrize(
"surface", ["background", "backgroundPanel", "backgroundElement", "backgroundCode"]
)
def test_transparent_text_is_readable_on_each_surface(palette, role, surface):
theme = json.loads(opencode_content(palette, transparent_background=True))["theme"]
background = palette["bg"] if theme[surface] == "none" else theme[surface]
assert contrast(theme[role], background) >= 4.5


@pytest.mark.parametrize(
"token,role", [("muted", "textMuted"), ("subtle", "textSubtle"), ("comment", "textPlaceholder")]
)
def test_transparent_secondary_text_is_guarded_without_changing_palette(palette, token, role):
palette[token] = palette["bg_soft"]
original = palette.copy()
theme = json.loads(opencode_content(palette, transparent_background=True))["theme"]
for background in (palette["bg"], palette["bg_soft"]):
assert contrast(theme[role], background) >= 4.5
assert palette == original


def test_transparent_hover_selection_and_syntax_are_preserved(palette):
opaque = json.loads(opencode_content(palette))
transparent = json.loads(opencode_content(palette, transparent_background=True))
assert transparent.keys() == opaque.keys()
assert transparent["defs"] == opaque["defs"]
assert transparent["theme"].keys() == opaque["theme"].keys()
for key, value in opaque["theme"].items():
corrected = detect_mode(palette) == "light" and key in (
"syntaxComment",
"syntaxFunction",
"syntaxMethod",
"syntaxOperator",
"syntaxDecorator",
)
if corrected:
assert transparent["theme"][key] == guard(
value,
transparent["theme"]["backgroundCode"],
"light",
minimum=4.5,
)
elif key.startswith("syntax") or key in (
"backgroundHover",
"backgroundSelected",
"textSelected",
):
assert transparent["theme"][key] == value
assert transparent["theme"]["backgroundHover"] != palette["bg_soft"]
assert transparent["theme"]["backgroundSelected"] != palette["bg_soft"]


def test_nontransparent_surface_and_text_formulas_are_preserved(palette):
theme = json.loads(opencode_content(palette))["theme"]
dark = detect_mode(palette) == "dark"
assert theme["background"] == palette["bg"]
assert theme["backgroundPanel"] == palette["surface1" if dark else "surface0"]
assert theme["backgroundElement"] == (
palette["surface1"] if dark else mix(palette["bg_soft"], palette["surface1"], 0.4)
)
assert theme["backgroundCode"] == palette["surface1" if dark else "surface0"]
assert theme["backgroundLine"] == palette["surface1" if dark else "bg_soft"]
assert theme["text"] == palette["text"]
assert theme["textMuted"] == palette["muted"]
assert theme["textSubtle"] == guard(
palette["subtle"], palette["bg"], detect_mode(palette), minimum=5.5
)
assert theme["textPlaceholder"] == (
palette["comment"]
if dark
else guard(
palette["comment"],
theme["backgroundElement"],
"light",
minimum=4.5,
)
)
3 changes: 2 additions & 1 deletion tests/test_pi_theme_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ def test_opencode_preserves_terminal_transparent_background(self):
theme = json.loads(theme_path.read_text())["theme"]

self.assertEqual(theme["background"], "none")
self.assertEqual(theme["backgroundPanel"], "#0D0D0F")
self.assertEqual(theme["backgroundPanel"], "none")
self.assertEqual(theme["backgroundElement"], "#0B0B0B")
self.assertEqual(theme["primary"], "#A5B4FC")
self.assertEqual(theme["secondary"], "#D4B5FD")

Expand Down
Loading