diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 3e477b75..7c5688de 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -45,10 +45,10 @@ jobs: ln -sf "$PWD/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish" ~/.config/fish/conf.d/05-dreamcoder-theme.fish # Deploy theme variant files from the repo, mirroring the real # install (ghostty dual-mode and kitty theme switching reference - # the -dark/-light/-night variants at runtime). + # the -dark/-light variants at runtime). rm -rf ~/.config/ghostty/themes ln -sfn "$PWD/DreamcoderGhostty/.config/ghostty/themes" ~/.config/ghostty/themes - for f in colors-dreamcoder.conf colors-dreamcoder-dark.conf colors-dreamcoder-light.conf colors-dreamcoder-night.conf; do + for f in colors-dreamcoder.conf colors-dreamcoder-dark.conf colors-dreamcoder-light.conf; do ln -sf "$PWD/DreamcoderKitty/.config/kitty/$f" ~/.config/kitty/$f done diff --git a/AGENTS.md b/AGENTS.md index 519d96f0..3bd212ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ When editing documentation, follow these maintenance rules: - Run `python scripts/validate-markdown-links.py` after doc changes (pre-commit does this automatically for staged files). - Docs are English, user-facing guides in neutral professional English. - Identity is always "Dreamcoder Workbench" (never "Dreamcoder OS" / "DreamcoderDots" / bare "dreamcoder-dots"). -- Color modes are `dark/light/night` (never "dusk" as a user-facing mode). +- Color modes are `dark/light` (never "dusk" as a user-facing mode). - One file = one purpose: do not duplicate content across docs — link to the source page. - Update the smallest relevant page for a change; keep every doc reachable from [docs/README.md](docs/README.md). - Commit doc changes with the `docs:` conventional commit type. diff --git a/CLAUDE.md b/CLAUDE.md index 8426a853..1078a863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,9 +55,9 @@ focus #3B82F6 opacity 0.76 ``` -Dark and Light are the user-facing modes. Night is a derived render profile of -Dark; legacy Dusk remains a compatibility token set, and OLED behavior is defined -by `modes.dark.surface_policy`. +Dark and Light are the only user-facing modes. Legacy Dusk remains a +compatibility token set, and OLED behavior is defined by +`modes.dark.surface_policy`. **Light — Cocoa/Lúcuma** diff --git a/COMPARISON.md b/COMPARISON.md index a23767ee..1fa13083 100644 --- a/COMPARISON.md +++ b/COMPARISON.md @@ -8,7 +8,7 @@ | Feature | Gentleman.Dots | ML4W | **Dreamcoder Workbench** | | --------------------- | ---------------------- | ------------------- | ------------------------------ | | **Theme Engine** | — Catppuccin static | ✓ Matugen dynamic | ✓ **Token-based + WCAG/APCA** | -| **Dark/Light/Night** | — Dark only | ✓ Light/Dark | ✓ **+ night transition mode** | +| **Dark/Light** | — Dark only | ✓ Light/Dark | ✓ **+ scheduled switching** | | **Accessibility** | — | — | ✓ **WCAG 4.5:1 + APCA 75** | | **AI Session Prompt** | — | — | ✓ **Live AI session state** | | **Neovim Plugins** | ✓ 29 (LazyVim) | — | ◐ Dreamcoder colorscheme | @@ -70,13 +70,12 @@ After installing Dreamcoder Workbench, everything Gentleman provides keeps worki No other dotfiles setup does this. Every color is validated against WCAG 4.5:1 and APCA. -### 3 Color Modes +### 2 Color Modes | Mode | Name | Schedule | | ------- | ----------------- | -------------- | | 🌙 Dark | Anthracite Steel | 18:00-07:00 | -| ☀️ Light| Cocoa/Lúcuma | 07:00-16:00 | -| 🌆 Night| Low-light transition | 16:00-18:00 | +| ☀️ Light| Cocoa/Lúcuma | 07:00-18:00 | ### AI Session State in the Prompt @@ -139,7 +138,7 @@ Without Dreamcoder Workbench: ┌─────────────────────────────────────────────┐ │ Gentleman.Dots: Catppuccin │ │ ML4W: Matugen colors │ -│ No WCAG, no AI prompt, no night mode │ +│ No WCAG, no AI prompt, no auto switching │ └─────────────────────────────────────────────┘ With Dreamcoder Workbench: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 81e3955d..752996af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Need a smaller first step? Pick an open issue, fix a docs typo, or add a rendere ```text src/dreamcoder_theme/ # Python theme engine (pip-installable package) ├── palette.py # Color math, adaptive palette derivation - ├── palette_tokens.py # Static design tokens (dark/light/night) + ├── palette_tokens.py # Static design tokens (dark/light/dusk) ├── renderers.py # Public renderer import hub ├── renderers_kitty.py # Kitty format renderer ├── renderers_ghostty_warp.py @@ -51,7 +51,7 @@ src/dreamcoder_theme/ # Python theme engine (pip-installable package) ├── backups.py # Theme backup/restore ├── audit.py # Theme audit & validation ├── docs_report.py # Documentation health report - ├── motion.py # Day/night mode scheduler + ├── motion.py # Motion preset scheduler ├── tui.py # Terminal UI ├── visual_regression.py # Visual regression testing └── settings_store.py # Settings persistence @@ -201,7 +201,7 @@ Documentation changes are reviewed like code changes. - [ ] Run `python scripts/validate-markdown-links.py` and fix any broken links. - [ ] Update the smallest relevant page; do not duplicate content that already lives in another doc — link to it instead. - [ ] The changed page is reachable from the [docs index](docs/README.md) or the README documentation table; add it there if it is a new page. -- [ ] Keep the identity string "Dreamcoder Workbench" and mode names `dark/light/night` consistent with the rest of the docs. +- [ ] Keep the identity string "Dreamcoder Workbench" and mode names `dark/light` consistent with the rest of the docs. - [ ] Commit with the `docs:` conventional commit type. Do not duplicate references: link to the source page for detailed behavior. diff --git a/DreamcoderAntigravity/Dreamcoder-Night.json b/DreamcoderAntigravity/Dreamcoder-Night.json deleted file mode 100644 index 1caae53a..00000000 --- a/DreamcoderAntigravity/Dreamcoder-Night.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "name": "Dreamcoder Dark Night", - "type": "dark", - "colors": { - "editor.background": "#000000", - "editor.foreground": "#c6c6c6", - "activityBar.background": "#090909", - "activityBar.foreground": "#a4afee", - "activityBar.inactiveForeground": "#b3b3b3", - "activityBar.border": "#797979", - "sideBar.background": "#090909", - "sideBar.foreground": "#c6c6c6", - "sideBar.border": "#797979", - "statusBar.background": "#000000", - "statusBar.foreground": "#c6c6c6", - "statusBar.border": "#797979", - "editorGroupHeader.tabsBackground": "#090909", - "tab.activeBackground": "#000000", - "tab.activeForeground": "#a4afee", - "tab.inactiveBackground": "#090909", - "tab.inactiveForeground": "#b3b3b3", - "tab.border": "#797979", - "editor.lineHighlightBackground": "#090909", - "editorLineNumber.foreground": "#b3b3b3", - "editorLineNumber.activeForeground": "#a4afee", - "editorWidget.background": "#0b0b0d", - "editorWidget.border": "#797979", - "input.background": "#0b0b0d", - "input.foreground": "#c6c6c6", - "input.border": "#3a76d6", - "button.background": "#b48ce9", - "button.foreground": "#000000", - "list.activeSelectionBackground": "#0b0b0d", - "list.activeSelectionForeground": "#c6c6c6", - "list.hoverBackground": "#090909", - "editor.selectionBackground": "#0b0b0d", - "terminal.background": "#000000", - "terminal.foreground": "#c6c6c6", - "terminal.ansiBlack": "#090909", - "terminal.ansiRed": "#e88484", - "terminal.ansiGreen": "#43b48b", - "terminal.ansiYellow": "#d1a126", - "terminal.ansiBlue": "#a4afee", - "terminal.ansiMagenta": "#b98beb", - "terminal.ansiCyan": "#62b9e2", - "terminal.ansiWhite": "#c6c6c6" - }, - "tokenColors": [ - { - "scope": [ - "comment", - "punctuation.definition.comment" - ], - "settings": { - "foreground": "#b3b3b3", - "fontStyle": "italic" - } - }, - { - "scope": [ - "keyword", - "storage.type", - "storage.modifier", - "keyword.operator" - ], - "settings": { - "foreground": "#a4afee", - "fontStyle": "bold" - } - }, - { - "scope": [ - "entity.name.function", - "support.function", - "entity.name.method" - ], - "settings": { - "foreground": "#62b9e2" - } - }, - { - "scope": [ - "string", - "punctuation.definition.string" - ], - "settings": { - "foreground": "#43b48b" - } - }, - { - "scope": [ - "constant.numeric", - "constant.language" - ], - "settings": { - "foreground": "#b48ce9" - } - }, - { - "scope": [ - "support.type", - "entity.name.type", - "entity.name.class" - ], - "settings": { - "foreground": "#b48ce9" - } - }, - { - "scope": [ - "variable", - "meta.definition.variable" - ], - "settings": { - "foreground": "#c6c6c6" - } - } - ] -} diff --git a/DreamcoderBat/.config/bat/themes/Dreamcoder-Night.tmTheme b/DreamcoderBat/.config/bat/themes/Dreamcoder-Night.tmTheme deleted file mode 100644 index d43863aa..00000000 --- a/DreamcoderBat/.config/bat/themes/Dreamcoder-Night.tmTheme +++ /dev/null @@ -1,39 +0,0 @@ - - - - - nameDreamcoder - settings - - settings - background#000000 - foreground#c6c6c6 - caret#a4afee - selection#323232 - selectionForeground#c6c6c6 - lineHighlight#0b0b0d - gutter#1b1b1b - gutterForeground#ababab - invisibles#b3b3b3 - - scopecommentsettingsforeground#b3b3b3fontStyleitalic - scopekeyword, storagesettingsforeground#a4afeefontStylebold - scopeentity.name.function, support.functionsettingsforeground#b48ce9 - scopevariable, meta.definition.variablesettingsforeground#ababab - scopestringsettingsforeground#43b48b - scopeconstant.numeric, constant.languagesettingsforeground#b98beb - scopeentity.name.type, support.typesettingsforeground#62b9e2 - scopepunctuationsettingsforeground#ababab - scopekeyword.operatorsettingsforeground#b48ce9 - scopeconstant.character.escapesettingsforeground#43b48b - scopeentity.name.tagsettingsforeground#a4afee - scopeentity.other.attribute-namesettingsforeground#66bae1 - scopeinvalidsettingsforeground#e88484 - scopemarkup.heading, markup.boldsettingsforeground#a4afeefontStylebold - scopemarkup.italicsettingsforeground#62b9e2fontStyleitalic - scopemeta.diff, meta.diff.headersettingsforeground#b48ce9 - scopesupport.class, support.constantsettingsforeground#62b9e2 - - - diff --git a/DreamcoderCodexApp/Dreamcoder-Night.codex-theme.json b/DreamcoderCodexApp/Dreamcoder-Night.codex-theme.json deleted file mode 100644 index f665e799..00000000 --- a/DreamcoderCodexApp/Dreamcoder-Night.codex-theme.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "$schema": "https://opencode.ai/theme.json", - "defs": { - "dreamBackground": "#000000", - "dreamPanel": "#090909", - "dreamElement": "#090909", - "dreamText": "#c6c6c6", - "dreamMuted": "#ababab", - "dreamCocoa": "#b48ce9", - "dreamLucuma": "#a4afee", - "dreamDiagnostic": "#62b9e2", - "dreamSage": "#43b48b", - "dreamViolet": "#b48ce9", - "dreamMauve": "#b98beb", - "dreamCoral": "#e88484", - "dreamWarning": "#d1a126" - }, - "theme": { - "background": "#000000", - "backgroundPanel": "#0b0b0d", - "backgroundElement": "#0b0b0d", - "backgroundHover": "#323232", - "backgroundSelected": "#323232", - "textSelected": "#c6c6c6", - "backgroundCode": "#0b0b0d", - "backgroundSearch": "#795d16", - "backgroundLine": "#0b0b0d", - "backgroundAssistant": "#0c161b", - "backgroundUser": "#14151d", - "backgroundTool": "#16111c", - "text": "#c6c6c6", - "textMuted": "#ababab", - "textSubtle": "#959595", - "textPlaceholder": "#b3b3b3", - "textAssistant": "#74bbdd", - "textUser": "#a9b2e8", - "textTool": "#62b9e2", - "primary": "#a4afee", - "secondary": "#b48ce9", - "accent": "#a4afee", - "accentMuted": "#7b83b2", - "error": "#e88484", - "warning": "#d1a126", - "success": "#43b48b", - "info": "#62b9e2", - "border": "#797979", - "borderActive": "#a4afee", - "borderSubtle": "#5c5c5c", - "borderFocus": "#3a76d6", - "shadow": "#000000", - "diffAdded": "#43b48b", - "diffRemoved": "#e88484", - "diffContext": "#ababab", - "diffHunkHeader": "#b48ce9", - "diffHighlightAdded": "#43b48b", - "diffHighlightRemoved": "#e88484", - "diffAddedBg": "#284a3e", - "diffRemovedBg": "#5a3c3c", - "diffContextBg": "#000000", - "diffLineNumber": "#959595", - "diffAddedLineNumberBg": "#284a3e", - "diffRemovedLineNumberBg": "#5a3c3c", - "diffHunkHeaderBg": "#514561", - "diffFold": "#b3b3b3", - "diffFoldBg": "#0b0b0d", - "markdownText": "#c6c6c6", - "markdownHeading": "#a4afee", - "markdownLink": "#62b9e2", - "markdownLinkText": "#a4afee", - "markdownCode": "#43b48b", - "markdownBlockQuote": "#b48ce9", - "markdownEmph": "#62b9e2", - "markdownStrong": "#a4afee", - "markdownHorizontalRule": "#797979", - "markdownListItem": "#a4afee", - "markdownListEnumeration": "#b48ce9", - "markdownTableBorder": "#797979", - "markdownTableHeader": "#b48ce9", - "markdownImage": "#b98beb", - "markdownImageText": "#c6c6c6", - "markdownCodeBlock": "#c6c6c6", - "markdownCodeBlockBg": "#0b0b0d", - "markdownInlineCodeBg": "#5b9983", - "syntaxComment": "#b3b3b3", - "syntaxKeyword": "#a4afee", - "syntaxFunction": "#b48ce9", - "syntaxMethod": "#b48ce9", - "syntaxVariable": "#ababab", - "syntaxParameter": "#b58fe7", - "syntaxProperty": "#66bae1", - "syntaxField": "#4ab58e", - "syntaxString": "#43b48b", - "syntaxNumber": "#ad9fed", - "syntaxBoolean": "#b98beb", - "syntaxConstant": "#b98beb", - "syntaxType": "#62b9e2", - "syntaxClass": "#6eb7e4", - "syntaxInterface": "#62b9e2", - "syntaxEnum": "#5cb8d2", - "syntaxOperator": "#b48ce9", - "syntaxPunctuation": "#ababab", - "syntaxTag": "#a4afee", - "syntaxAttribute": "#66bae1", - "syntaxRegexp": "#c689ce", - "syntaxEscape": "#d1a126", - "syntaxNamespace": "#62b9e2", - "syntaxModule": "#62b9e2", - "syntaxDecorator": "#b48ce9", - "syntaxBuiltin": "#b98beb", - "syntaxSpecial": "#d1a126", - "syntaxTodo": "#d0a539", - "syntaxDeprecated": "#d78f8f", - "terminalBlack": "#777777", - "terminalRed": "#e88484", - "terminalGreen": "#43b48b", - "terminalYellow": "#d1a126", - "terminalBlue": "#62b9e2", - "terminalMagenta": "#b98beb", - "terminalCyan": "#74bbdd", - "terminalWhite": "#c6c6c6", - "terminalBrightBlack": "#959595", - "terminalBrightRed": "#e29090", - "terminalBrightGreen": "#5bb796", - "terminalBrightYellow": "#cfa740", - "terminalBrightBlue": "#74bbdd", - "terminalBrightMagenta": "#bb96e4", - "terminalBrightCyan": "#b796e3", - "terminalBrightWhite": "#c6c6c6" - } -} diff --git a/DreamcoderCodexCLI/Dreamcoder-Night.tmTheme b/DreamcoderCodexCLI/Dreamcoder-Night.tmTheme deleted file mode 100644 index d43863aa..00000000 --- a/DreamcoderCodexCLI/Dreamcoder-Night.tmTheme +++ /dev/null @@ -1,39 +0,0 @@ - - - - - nameDreamcoder - settings - - settings - background#000000 - foreground#c6c6c6 - caret#a4afee - selection#323232 - selectionForeground#c6c6c6 - lineHighlight#0b0b0d - gutter#1b1b1b - gutterForeground#ababab - invisibles#b3b3b3 - - scopecommentsettingsforeground#b3b3b3fontStyleitalic - scopekeyword, storagesettingsforeground#a4afeefontStylebold - scopeentity.name.function, support.functionsettingsforeground#b48ce9 - scopevariable, meta.definition.variablesettingsforeground#ababab - scopestringsettingsforeground#43b48b - scopeconstant.numeric, constant.languagesettingsforeground#b98beb - scopeentity.name.type, support.typesettingsforeground#62b9e2 - scopepunctuationsettingsforeground#ababab - scopekeyword.operatorsettingsforeground#b48ce9 - scopeconstant.character.escapesettingsforeground#43b48b - scopeentity.name.tagsettingsforeground#a4afee - scopeentity.other.attribute-namesettingsforeground#66bae1 - scopeinvalidsettingsforeground#e88484 - scopemarkup.heading, markup.boldsettingsforeground#a4afeefontStylebold - scopemarkup.italicsettingsforeground#62b9e2fontStyleitalic - scopemeta.diff, meta.diff.headersettingsforeground#b48ce9 - scopesupport.class, support.constantsettingsforeground#62b9e2 - - - diff --git a/DreamcoderFastfetch/.config/dreamcoder/settings.json b/DreamcoderFastfetch/.config/dreamcoder/settings.json index 60947351..4fc4e027 100644 --- a/DreamcoderFastfetch/.config/dreamcoder/settings.json +++ b/DreamcoderFastfetch/.config/dreamcoder/settings.json @@ -1,8 +1,5 @@ { "terminal": { "default_mode": "dark" - }, - "theme": { - "render_profile": "standard" } } diff --git a/DreamcoderGhostty/.config/ghostty/themes/dreamcoder-night b/DreamcoderGhostty/.config/ghostty/themes/dreamcoder-night deleted file mode 100644 index c766ea77..00000000 --- a/DreamcoderGhostty/.config/ghostty/themes/dreamcoder-night +++ /dev/null @@ -1,27 +0,0 @@ -# Dreamcoder Dark Night -background = #000000 -foreground = #c6c6c6 -cursor-color = #a4afee -cursor-text = #000000 -selection-background = #323232 -selection-foreground = #c6c6c6 -background-opacity = 0.76 -background-blur = true -minimum-contrast = 4.5 - -palette = 0=#777777 -palette = 1=#e88484 -palette = 2=#43b48b -palette = 3=#d1a126 -palette = 4=#62b9e2 -palette = 5=#b98beb -palette = 6=#b48ce9 -palette = 7=#ababab -palette = 8=#959595 -palette = 9=#e29090 -palette = 10=#5bb796 -palette = 11=#cfa843 -palette = 12=#74bbdd -palette = 13=#bb96e4 -palette = 14=#5384d3 -palette = 15=#c6c6c6 diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.7.3/config.night.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.7.3/config.night.toml deleted file mode 100644 index 414e7494..00000000 --- a/DreamcoderHerdr/.config/herdr/dreamcoder/0.7.3/config.night.toml +++ /dev/null @@ -1,30 +0,0 @@ -# Managed by Dreamcoder; repository variant only. -[theme] -name = "catppuccin" - -[theme.custom] -accent = "#a4afee" -panel_bg = "#000000" -surface0 = "#090909" -surface1 = "#0b0b0d" -surface_dim = "#090909" -overlay0 = "#797979" -overlay1 = "#959595" -text = "#c6c6c6" -subtext0 = "#ababab" -mauve = "#b98beb" -green = "#43b48b" -yellow = "#d1a126" -red = "#e88484" -blue = "#62b9e2" -teal = "#3a76d6" -peach = "#b48ce9" - -[ui] -accent = "#6FA0AF" - -[keys] -prefix = "ctrl+a" -previous_agent = "prefix+alt+k" -next_agent = "prefix+alt+j" -focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.0/config.night.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.0/config.night.toml deleted file mode 100644 index f4f6ac26..00000000 --- a/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.0/config.night.toml +++ /dev/null @@ -1,31 +0,0 @@ -# Managed by Dreamcoder; repository variant only. -[theme] -name = "catppuccin" - -[theme.custom] -accent = "#a4afee" -panel_bg = "#000000" -surface0 = "#090909" -surface1 = "#0b0b0d" -surface_dim = "#090909" -overlay0 = "#797979" -overlay1 = "#959595" -text = "#c6c6c6" -subtext0 = "#ababab" -mauve = "#b98beb" -green = "#43b48b" -yellow = "#d1a126" -red = "#e88484" -blue = "#62b9e2" -teal = "#3a76d6" -peach = "#b48ce9" - -[ui] -accent = "#6FA0AF" -pane_scrollbars = false - -[keys] -prefix = "ctrl+a" -previous_agent = "prefix+alt+k" -next_agent = "prefix+alt+j" -focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.2/config.night.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.2/config.night.toml deleted file mode 100644 index 57e50f0b..00000000 --- a/DreamcoderHerdr/.config/herdr/dreamcoder/0.8.2/config.night.toml +++ /dev/null @@ -1,34 +0,0 @@ -# Managed by Dreamcoder; repository variant only. -[theme] -name = "catppuccin" - -[theme.custom] -accent = "#a4afee" -panel_bg = "#000000" -surface0 = "#090909" -surface1 = "#0b0b0d" -surface_dim = "#090909" -overlay0 = "#797979" -overlay1 = "#959595" -text = "#c6c6c6" -subtext0 = "#ababab" -mauve = "#b98beb" -green = "#43b48b" -yellow = "#d1a126" -red = "#e88484" -blue = "#62b9e2" -teal = "#3a76d6" -peach = "#b48ce9" -sidebar_bg = "#000000" -active_row_bg = "#090909" -selection_bg = "#323232" - -[ui] -accent = "#6FA0AF" -pane_scrollbars = false - -[keys] -prefix = "ctrl+a" -previous_agent = "prefix+alt+k" -next_agent = "prefix+alt+j" -focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml b/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml deleted file mode 100644 index 57e50f0b..00000000 --- a/DreamcoderHerdr/.config/herdr/dreamcoder/0.9.1/config.night.toml +++ /dev/null @@ -1,34 +0,0 @@ -# Managed by Dreamcoder; repository variant only. -[theme] -name = "catppuccin" - -[theme.custom] -accent = "#a4afee" -panel_bg = "#000000" -surface0 = "#090909" -surface1 = "#0b0b0d" -surface_dim = "#090909" -overlay0 = "#797979" -overlay1 = "#959595" -text = "#c6c6c6" -subtext0 = "#ababab" -mauve = "#b98beb" -green = "#43b48b" -yellow = "#d1a126" -red = "#e88484" -blue = "#62b9e2" -teal = "#3a76d6" -peach = "#b48ce9" -sidebar_bg = "#000000" -active_row_bg = "#090909" -selection_bg = "#323232" - -[ui] -accent = "#6FA0AF" -pane_scrollbars = false - -[keys] -prefix = "ctrl+a" -previous_agent = "prefix+alt+k" -next_agent = "prefix+alt+j" -focus_agent = "prefix+ctrl+1..9" diff --git a/DreamcoderKitty/.config/kitty/colors-dreamcoder-night.conf b/DreamcoderKitty/.config/kitty/colors-dreamcoder-night.conf deleted file mode 100644 index 57d4fd45..00000000 --- a/DreamcoderKitty/.config/kitty/colors-dreamcoder-night.conf +++ /dev/null @@ -1,52 +0,0 @@ -# ========================================================== -# Dreamcoder Dark Night -# ========================================================== -# Color-only theme layer. Keep ML4W/Gentleman behavior elsewhere. - -foreground #c6c6c6 -background #000000 -selection_foreground #c6c6c6 -selection_background #323232 -url_color #a4afee - -cursor #a4afee -cursor_text_color #000000 -cursor_shape block -cursor_blink_interval 0.5 -cursor_stop_blinking_after 15.0 - -active_tab_foreground #000000 -active_tab_background #a4afee -inactive_tab_foreground #ababab -inactive_tab_background #000000 -tab_bar_background #000000 - -active_border_color #a4afee -inactive_border_color #5c5c5c -bell_border_color #e88484 - -color0 #777777 -color1 #e88484 -color2 #43b48b -color3 #d1a126 -color4 #62b9e2 -color5 #b98beb -color6 #b48ce9 -color7 #ababab -color8 #959595 -color9 #e29090 -color10 #5bb796 -color11 #cfa843 -color12 #74bbdd -color13 #bb96e4 -color14 #5384d3 -color15 #c6c6c6 -color16 #b48ce9 -color17 #e88484 - -mark1_foreground #000000 -mark1_background #a4afee -mark2_foreground #000000 -mark2_background #62b9e2 -mark3_foreground #000000 -mark3_background #b98beb diff --git a/DreamcoderKitty/.config/kitty/dreamcoder-ui-night.conf b/DreamcoderKitty/.config/kitty/dreamcoder-ui-night.conf deleted file mode 100644 index 45e53f9f..00000000 --- a/DreamcoderKitty/.config/kitty/dreamcoder-ui-night.conf +++ /dev/null @@ -1,51 +0,0 @@ -# Dreamcoder Kitty UI parity layer -# Loaded last so ML4W can keep behavior while Dreamcoder owns readability. -# Dark mode: glass blur (24px) with 76% opacity for depth. -# Light mode: opaque paper (96%) for maximum text legibility. -# text_composition_strategy: -# gamma (1st val): thickness of dark text on light bg (default 1.0 on Linux). -# contrast (2nd val): extra contrast %, 0-100 (default 0 on Linux). -# Higher gamma = thicker text on light themes; contrast helps both modes. - -font_family JetBrainsMono Nerd Font -bold_font auto -italic_font auto -bold_italic_font auto -font_size 14 -disable_ligatures cursor -symbol_map U+E000-U+F8FF,U+F0000-U+10FFFF Symbols Nerd Font -box_drawing_scale 0.001, 1, 1.5, 2 -# gamma 1.0 + 15% contrast = thicker, sharper text matching Ghostty -text_composition_strategy 1.0 15 -shell fish --login - -window_padding_width 20 18 20 18 -single_window_padding_width -1 -placement_strategy center -initial_window_width 1180 -initial_window_height 780 -background_opacity 0.76 -dynamic_background_opacity no -background_blur 24 - -tab_bar_edge top -tab_bar_style fade -tab_bar_min_tabs 2 -tab_bar_margin_color none -active_tab_font_style bold -inactive_tab_font_style normal - -shell_integration enabled no-cursor -mouse_hide_wait 2.0 -copy_on_select clipboard -dim_opacity 0.65 -inactive_text_alpha 0.92 - -# Dreamcoder terminal shortcuts. -map ctrl+l clear_terminal reset active -map ctrl+shift+l clear_terminal scrollback active -map ctrl+backspace send_text all \x17 -map ctrl+delete send_text all \ed - -# Dreamcoder Motion: ML4W-inspired cursor trail, kept local and portable. -cursor_trail 1 diff --git a/DreamcoderLazygit/.config/lazygit/config.night.yml b/DreamcoderLazygit/.config/lazygit/config.night.yml deleted file mode 100644 index c5644330..00000000 --- a/DreamcoderLazygit/.config/lazygit/config.night.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Dreamcoder Dark Night — Lazygit theme -# https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md - -gui: - theme: - activeBorderColor: - - "#a4afee" # accent - - "bold" - inactiveBorderColor: - - "#5c5c5c" # border - searchingActiveBorderColor: - - "#b48ce9" # accent-2 - - "bold" - optionsTextColor: - - "#62b9e2" # diagnostic - selectedLineBgColor: - - "#323232" # selection - inactiveViewSelectedLineBgColor: - - "#1b1b1b" # surface-2 - cherryPickedCommitFgColor: - - "#62b9e2" - cherryPickedCommitBgColor: - - "#b48ce9" - markedBaseCommitFgColor: - - "#62b9e2" - markedBaseCommitBgColor: - - "#a4afee" - unstagedChangesColor: - - "#e88484" # error - defaultFgColor: - - "#c6c6c6" # text - authorColors: - "#a4afee": "#000000" - "#b48ce9": "#000000" - "#62b9e2": "#000000" - "#43b48b": "#000000" - "#b98beb": "#000000" - showFileTree: true - showRandomTip: false - nerdFontsVersion: "3" - sidePanelWidth: 0.3 - expandFocusedSidePanel: true - mainPanelSplitMode: "flexible" - language: "en" -git: - diffRenderers: - - colorArg: always - command: delta --syntax-theme "Catppuccin Mocha" --paging=never - merging: - manualCommit: false - skipHookPrefix: WIP - autoFetch: true - autoRefresh: true - branchLogCmd: "git log --graph --pretty=format:'%C(#a4afee)%h%Creset -%C(#b48ce9)%d%Creset %s %C(#b3b3b3)(%cr) %C(#62b9e2)<%an>%Creset' --date=relative" -update: - method: never diff --git a/DreamcoderNvim/.config/nvim/colors/dreamcoder-night.lua b/DreamcoderNvim/.config/nvim/colors/dreamcoder-night.lua deleted file mode 100644 index 8b6d6876..00000000 --- a/DreamcoderNvim/.config/nvim/colors/dreamcoder-night.lua +++ /dev/null @@ -1,1263 +0,0 @@ --- ======================================================== --- Dreamcoder Dark Night — generated by Dreamcoder sync --- ======================================================== --- Loaded by the dreamcoder dispatcher (colors/dreamcoder.lua). --- Can also be loaded directly: vim.cmd.colorscheme("dreamcoder-night") --- ======================================================== - -vim.g.colors_name = "dreamcoder-night" - -local c = { - bg = "#000000", - surface0 = "#090909", - surface1 = "#0b0b0d", - surface2 = "#1b1b1b", - text = "#c6c6c6", - muted = "#ababab", - subtle = "#959595", - comment = "#b3b3b3", - accent = "#a4afee", - accent2 = "#b48ce9", - diagnostic = "#62b9e2", - sage = "#43b48b", - lavender = "#b48ce9", - mauve = "#b98beb", - error = "#e88484", - warning = "#d1a126", - border = "#5c5c5c", - border_ui = "#797979", - selection = "#323232", -} - -local function h(name, opts) - vim.api.nvim_set_hl(0, name, opts) -end - --- ── Editor UI ──────────────────────────────────────────────── --- Normal background is opaque in light so dark terminal transparency cannot hide text. --- Dreamcoder Dark keeps transparent Normal for the glass effect. - - vim.api.nvim_set_hl(0, "Normal", { - fg = "#c6c6c6", - bg = "none" - }) - - vim.api.nvim_set_hl(0, "NormalFloat", { - fg = "#c6c6c6", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "FloatBorder", { - fg = "#797979", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "FloatTitle", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "NonText", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "SpecialKey", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "Whitespace", { - fg = "#797979" - }) - - vim.api.nvim_set_hl(0, "EndOfBuffer", { - fg = "none" - }) - - vim.api.nvim_set_hl(0, "Cursor", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "lCursor", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "CursorLine", { - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "CursorLineNr", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "LineNr", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "CursorColumn", { - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "ColorColumn", { - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "SignColumn", { - bg = "none" - }) - - - -- ── Selection & Search ─────────────────────────────── - vim.api.nvim_set_hl(0, "Visual", { - fg = "#c6c6c6", - bg = "#323232" - }) - - vim.api.nvim_set_hl(0, "VisualNOS", { - fg = "#c6c6c6", - bg = "#323232" - }) - - vim.api.nvim_set_hl(0, "Search", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "IncSearch", { - fg = "#000000", - bg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CurSearch", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Substitute", { - fg = "#000000", - bg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "MatchParen", { - fg = "#b48ce9", - bold = true - }) - - - -- ── Popup Menu ──────────────────────────────────────── - vim.api.nvim_set_hl(0, "Pmenu", { - fg = "#ababab", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "PmenuSel", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "PmenuSbar", { - bg = "#1b1b1b" - }) - - vim.api.nvim_set_hl(0, "PmenuThumb", { - bg = "#ababab" - }) - - - -- ── Statusline & Winbar ─────────────────────────────── - vim.api.nvim_set_hl(0, "StatusLine", { - fg = "#c6c6c6", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "StatusLineNC", { - fg = "#ababab", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "StatusLineTerm", { - fg = "#c6c6c6", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "StatusLineTermNC", { - fg = "#ababab", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "WinBar", { - fg = "#c6c6c6", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "WinBarNC", { - fg = "#ababab", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "WinSeparator", { - fg = "#797979" - }) - - - -- ── Tabline ──────────────────────────────────────────── - vim.api.nvim_set_hl(0, "TabLine", { - fg = "#ababab", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "TabLineSel", { - fg = "#c6c6c6", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "TabLineFill", { - bg = "#000000" - }) - - - -- ── Folds ───────────────────────────────────────────── - vim.api.nvim_set_hl(0, "Folded", { - fg = "#ababab", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "FoldColumn", { - fg = "#ababab" - }) - - - -- ── Diff ────────────────────────────────────────────── - vim.api.nvim_set_hl(0, "DiffAdd", { - bg = "#1a3c30" - }) - - vim.api.nvim_set_hl(0, "DiffChange", { - bg = "#453712" - }) - - vim.api.nvim_set_hl(0, "DiffDelete", { - bg = "#4c2e2e" - }) - - vim.api.nvim_set_hl(0, "DiffText", { - bg = "#1a2f51" - }) - - - -- ── Spell ───────────────────────────────────────────── - vim.api.nvim_set_hl(0, "SpellBad", { - sp = "#e88484", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "SpellCap", { - sp = "#d1a126", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "SpellLocal", { - sp = "#62b9e2", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "SpellRare", { - sp = "#b98beb", - undercurl = true - }) - - - -- ── Messages ────────────────────────────────────────── - vim.api.nvim_set_hl(0, "MsgArea", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "ModeMsg", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "WarningMsg", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "ErrorMsg", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "Question", { - fg = "#62b9e2" - }) - - - -- ── Misc ────────────────────────────────────────────── - vim.api.nvim_set_hl(0, "Title", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "Directory", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "Conceal", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "TermCursor", { - fg = "#000000", - bg = "#a4afee" - }) - - - -- ── Classic Syntax ──────────────────────────────────── - vim.api.nvim_set_hl(0, "Comment", { - fg = "#b3b3b3", - italic = true - }) - - vim.api.nvim_set_hl(0, "Constant", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "String", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "Character", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "Number", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "Boolean", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "Float", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "Identifier", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "Function", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "Statement", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "Conditional", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Repeat", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Label", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Operator", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "Keyword", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Exception", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "PreProc", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "Include", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "Define", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "PreCondit", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "Type", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "StorageClass", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "Structure", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "Typedef", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "Special", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "SpecialChar", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "Delimiter", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "SpecialComment", { - fg = "#b3b3b3" - }) - - vim.api.nvim_set_hl(0, "Debug", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "Underlined", { - fg = "#62b9e2", - underline = true - }) - - vim.api.nvim_set_hl(0, "Ignore", { - fg = "#000000" - }) - - vim.api.nvim_set_hl(0, "Error", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "Todo", { - fg = "#d1a126", - bold = true - }) - - - -- ── Treesitter ──────────────────────────────────────── - vim.api.nvim_set_hl(0, "@variable", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "@variable.builtin", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@variable.parameter", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "@variable.member", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "@constant", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "@constant.builtin", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@constant.macro", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@module", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@module.builtin", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@label", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@string", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "@string.documentation", { - fg = "#43b48b", - italic = true - }) - - vim.api.nvim_set_hl(0, "@string.regex", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "@string.escape", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "@character", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "@character.special", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@number", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "@boolean", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@float", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "@function", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@function.builtin", { - fg = "#b48ce9", - bold = true - }) - - vim.api.nvim_set_hl(0, "@function.call", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@function.macro", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@function.method", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@function.method.call", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@parameter", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "@method", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@field", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@property", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@constructor", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@conditional", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@repeat", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@debug", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "@include", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "@operator", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@keyword", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@keyword.function", { - fg = "#a4afee", - italic = true - }) - - vim.api.nvim_set_hl(0, "@keyword.operator", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@keyword.return", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "@exception", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "@type", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@type.builtin", { - fg = "#62b9e2", - bold = true - }) - - vim.api.nvim_set_hl(0, "@type.definition", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@type.qualifier", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@storageclass", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@attribute", { - fg = "#959595" - }) - - vim.api.nvim_set_hl(0, "@symbol", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@punctuation.delimiter", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "@punctuation.bracket", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "@punctuation.special", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@comment", { - fg = "#b3b3b3", - italic = true - }) - - vim.api.nvim_set_hl(0, "@comment.error", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "@comment.warning", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "@comment.todo", { - fg = "#d1a126", - bold = true - }) - - vim.api.nvim_set_hl(0, "@comment.note", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@markup.strong", { - bold = true - }) - - vim.api.nvim_set_hl(0, "@markup.italic", { - italic = true - }) - - vim.api.nvim_set_hl(0, "@markup.strikethrough", { - strikethrough = true - }) - - vim.api.nvim_set_hl(0, "@markup.underline", { - underline = true - }) - - vim.api.nvim_set_hl(0, "@markup.heading", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "@markup.quote", { - fg = "#b3b3b3", - italic = true - }) - - vim.api.nvim_set_hl(0, "@markup.math", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "@markup.link", { - fg = "#a4afee", - underline = true - }) - - vim.api.nvim_set_hl(0, "@markup.link.label", { - fg = "#a4afee", - underline = true - }) - - vim.api.nvim_set_hl(0, "@markup.link.url", { - fg = "#b48ce9", - underline = true - }) - - vim.api.nvim_set_hl(0, "@markup.raw", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "@markup.list", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@diff.plus", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "@diff.minus", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "@diff.delta", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "@tag", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "@tag.attribute", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "@tag.delimiter", { - fg = "#ababab" - }) - - - -- ── LSP / Diagnostics ──────────────────────────────── - vim.api.nvim_set_hl(0, "DiagnosticError", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "DiagnosticWarn", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "DiagnosticInfo", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "DiagnosticHint", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "DiagnosticOk", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "DiagnosticUnderlineError", { - sp = "#e88484", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "DiagnosticUnderlineWarn", { - sp = "#d1a126", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "DiagnosticUnderlineInfo", { - sp = "#62b9e2", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "DiagnosticUnderlineHint", { - sp = "#b48ce9", - undercurl = true - }) - - vim.api.nvim_set_hl(0, "DiagnosticVirtualTextError", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "DiagnosticVirtualTextWarn", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "DiagnosticVirtualTextInfo", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "DiagnosticVirtualTextHint", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "DiagnosticFloatingError", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "DiagnosticFloatingWarn", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "DiagnosticFloatingInfo", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "DiagnosticFloatingHint", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "DiagnosticSignError", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "DiagnosticSignWarn", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "DiagnosticSignInfo", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "DiagnosticSignHint", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "LspReferenceText", { - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "LspReferenceRead", { - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "LspReferenceWrite", { - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "LspInlayHint", { - fg = "#b3b3b3", - bg = "#090909" - }) - - - -- ── Telescope ───────────────────────────────────────── - vim.api.nvim_set_hl(0, "TelescopeNormal", { - fg = "#c6c6c6", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "TelescopeBorder", { - fg = "#797979", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "TelescopeTitle", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "TelescopePromptNormal", { - fg = "#c6c6c6", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "TelescopePromptBorder", { - fg = "#797979", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "TelescopePromptTitle", { - fg = "#b48ce9", - bold = true - }) - - vim.api.nvim_set_hl(0, "TelescopeSelection", { - fg = "#c6c6c6", - bg = "#323232" - }) - - vim.api.nvim_set_hl(0, "TelescopeMultiSelection", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "TelescopeMatching", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "TelescopePreviewNormal", { - fg = "#c6c6c6", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "TelescopePreviewBorder", { - fg = "#797979", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "TelescopeResultsNormal", { - fg = "#c6c6c6", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "TelescopeResultsBorder", { - fg = "#797979", - bg = "#000000" - }) - - - -- ── NvimTree ───────────────────────────────────────── - vim.api.nvim_set_hl(0, "NvimTreeNormal", { - fg = "#c6c6c6", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "NvimTreeRootFolder", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "NvimTreeGitDirty", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "NvimTreeGitNew", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "NvimTreeGitDeleted", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "NvimTreeOpenedFile", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "NvimTreeSpecialFile", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "NvimTreeSymlink", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "NvimTreeFolderName", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "NvimTreeFolderIcon", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "NvimTreeEmptyFolderName", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "NvimTreeOpenedFolderName", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "NvimTreeImageFile", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "NvimTreeIndentMarker", { - fg = "#797979" - }) - - vim.api.nvim_set_hl(0, "NvimTreeWinSeparator", { - fg = "#797979", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "NvimTreeCursorLine", { - bg = "#090909" - }) - - - -- ── WhichKey ───────────────────────────────────────── - vim.api.nvim_set_hl(0, "WhichKey", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "WhichKeyGroup", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "WhichKeyDesc", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "WhichKeySeperator", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "WhichKeySeparator", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "WhichKeyFloat", { - fg = "#c6c6c6", - bg = "#090909" - }) - - vim.api.nvim_set_hl(0, "WhichKeyBorder", { - fg = "#797979" - }) - - vim.api.nvim_set_hl(0, "WhichKeyValue", { - fg = "#b98beb" - }) - - - -- ── Lazy / Noice / Cmp ─────────────────────────────── - vim.api.nvim_set_hl(0, "LazyNormal", { - fg = "#c6c6c6", - bg = "#000000" - }) - - vim.api.nvim_set_hl(0, "LazyReasonPlugin", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "LazyReasonStart", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "LazyReasonSource", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "LazyReasonRt", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "LazyReasonCmd", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "LazyValue", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "LazyCommit", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "LazySpecial", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "LazyDir", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "LazyH1", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "LazyButton", { - fg = "#c6c6c6", - bg = "#0b0b0d" - }) - - vim.api.nvim_set_hl(0, "LazyButtonActive", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "NoiceMsg", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "NoiceCursor", { - fg = "#000000", - bg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "MiniCompletionActiveParameter", { - underline = true - }) - - vim.api.nvim_set_hl(0, "CmpItemAbbr", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "CmpItemAbbrMatch", { - fg = "#a4afee", - bold = true - }) - - vim.api.nvim_set_hl(0, "CmpItemAbbrMatchFuzzy", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "CmpItemKind", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "CmpItemMenu", { - fg = "#ababab" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindSnippet", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindKeyword", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindText", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindMethod", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindFunction", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindConstructor", { - fg = "#a4afee" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindField", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindVariable", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindClass", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindInterface", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindModule", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindProperty", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindUnit", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindValue", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindEnum", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindEnumMember", { - fg = "#b98beb" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindColor", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindFile", { - fg = "#c6c6c6" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindReference", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindFolder", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindEvent", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindOperator", { - fg = "#b48ce9" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindTypeParameter", { - fg = "#62b9e2" - }) - - vim.api.nvim_set_hl(0, "CmpItemKindCopied", { - fg = "#43b48b" - }) - - - -- ── GitSigns ──────────────────────────────────────── - vim.api.nvim_set_hl(0, "GitSignsAdd", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "GitSignsChange", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "GitSignsDelete", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "GitSignsAddNr", { - fg = "#43b48b" - }) - - vim.api.nvim_set_hl(0, "GitSignsChangeNr", { - fg = "#d1a126" - }) - - vim.api.nvim_set_hl(0, "GitSignsDeleteNr", { - fg = "#e88484" - }) - - vim.api.nvim_set_hl(0, "GitSignsAddLn", { - bg = "#050e0b" - }) - - vim.api.nvim_set_hl(0, "GitSignsChangeLn", { - bg = "#110d03" - }) - - vim.api.nvim_set_hl(0, "GitSignsDeleteLn", { - bg = "#130b0b" - }) - - vim.api.nvim_set_hl(0, "GitSignsCurrentLineBlame", { - fg = "#ababab" - }) diff --git a/DreamcoderNvim/.config/nvim/colors/dreamcoder.lua b/DreamcoderNvim/.config/nvim/colors/dreamcoder.lua index 24a4de39..85dd9418 100644 --- a/DreamcoderNvim/.config/nvim/colors/dreamcoder.lua +++ b/DreamcoderNvim/.config/nvim/colors/dreamcoder.lua @@ -3,12 +3,11 @@ -- ======================================================== -- Usage: vim.cmd.colorscheme("dreamcoder") -- Auto-loads the correct variant based on: --- 1. DREAMCODER_THEME_PROFILE env var (night) — before base mode --- 2. DREAMCODER_THEME_MODE env var (set by `dreamcoder light`) --- 3. ~/.cache/dreamcoder/cursor-cli.env (persisted by apply-theme-mode.sh) --- 4. vim.o.background (Neovim's own setting) --- 5. Fallback: dark --- Direct variant access: colorscheme dreamcoder-dark / dreamcoder-light / dreamcoder-night +-- 1. DREAMCODER_THEME_MODE env var (set by `dreamcoder light`) +-- 2. ~/.cache/dreamcoder/cursor-cli.env (persisted by apply-theme-mode.sh) +-- 3. vim.o.background (Neovim's own setting) +-- 4. Fallback: dark +-- Direct variant access: colorscheme dreamcoder-dark / dreamcoder-light -- ======================================================== vim.g.colors_name = "dreamcoder" @@ -21,29 +20,6 @@ vim.opt.pumblend = 10 local src = debug.getinfo(1, "S").source:match("@?(.*)") local theme_dir = src:match("^(.*[/\\])") or "." --- Resolve render profile first (night wins over base mode), then mode. -local profile = vim.env.DREAMCODER_THEME_PROFILE - -if not profile then - local cache_file = vim.fn.expand("~/.cache/dreamcoder/cursor-cli.env") - local f = io.open(cache_file, "r") - if f then - for line in f:lines() do - local m = line:match('^export DREAMCODER_THEME_PROFILE="(.-)"') - if m then - profile = m - break - end - end - f:close() - end -end - -if profile == "night" then - dofile(theme_dir .. "dreamcoder-night.lua") - return -end - -- Resolve base mode: env var > cache file > vim.o.background > dark local mode = vim.env.DREAMCODER_THEME_MODE diff --git a/DreamcoderPi/.pi/agent/scripts/pi-theme.sh b/DreamcoderPi/.pi/agent/scripts/pi-theme.sh index 39a26a09..6dbeb605 100755 --- a/DreamcoderPi/.pi/agent/scripts/pi-theme.sh +++ b/DreamcoderPi/.pi/agent/scripts/pi-theme.sh @@ -38,27 +38,11 @@ detect() { } mode="$(detect)" -profile="${DREAMCODER_THEME_PROFILE:-standard}" -case "${profile}" in -standard | night) ;; -*) - echo "✗ invalid render profile: ${profile} (expected standard|night)" >&2 - exit 1 - ;; -esac -# Night is an orthogonal render profile on top of the base mode: the Pi -# selector picks the generated *-night sibling while the mode stays dark. -variant="${mode}" -[[ "${profile}" == "night" ]] && variant="night" -src="$DC/DreamcoderPi/.pi/agent/themes/dreamcoder-$variant.json" +src="$DC/DreamcoderPi/.pi/agent/themes/dreamcoder-$mode.json" [[ -f "$src" ]] || { echo "✗ $src not found" >&2 exit 1 } mkdir -p "$(dirname "$LN")" ln -sf "$src" "$LN" -if [[ "${profile}" == "night" ]]; then - echo "→ pi-theme: $mode (night)" -else - echo "→ pi-theme: $mode" -fi +echo "→ pi-theme: $mode" diff --git a/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json b/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json deleted file mode 100644 index 2c31c190..00000000 --- a/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", - "name": "dreamcoder", - "vars": { - "cocoa": "#b48ce9", - "lucuma": "#a4afee", - "diagnostic": "#62b9e2", - "sage": "#43b48b", - "mauve": "#b98beb", - "coral": "#e88484", - "warning": "#d1a126", - "muted": "#ababab", - "subtle": "#959595", - "comment": "#b3b3b3", - "borderUi": "#797979", - "borderMuted": "#5c5c5c" - }, - "colors": { - "accent": "lucuma", - "border": "borderUi", - "borderAccent": "diagnostic", - "borderMuted": "borderMuted", - "success": "sage", - "error": "coral", - "warning": "warning", - "muted": "muted", - "dim": "subtle", - "text": "", - "thinkingText": "muted", - "selectedBg": "#0b0b0d", - "userMessageBg": "#07070a", - "userMessageText": "", - "customMessageBg": "#090909", - "customMessageText": "", - "customMessageLabel": "lucuma", - "toolPendingBg": "#040404", - "toolSuccessBg": "#0c2019", - "toolErrorBg": "#251515", - "toolTitle": "lucuma", - "toolOutput": "", - "mdHeading": "lucuma", - "mdLink": "diagnostic", - "mdLinkUrl": "muted", - "mdCode": "sage", - "mdCodeBlock": "", - "mdCodeBlockBorder": "borderMuted", - "mdQuote": "cocoa", - "mdQuoteBorder": "borderMuted", - "mdHr": "borderMuted", - "mdListBullet": "lucuma", - "toolDiffAdded": "sage", - "toolDiffRemoved": "coral", - "toolDiffContext": "muted", - "syntaxComment": "comment", - "syntaxKeyword": "#a4afee", - "syntaxFunction": "#b48ce9", - "syntaxVariable": "#ababab", - "syntaxString": "#43b48b", - "syntaxNumber": "#ad9fed", - "syntaxType": "#62b9e2", - "syntaxOperator": "#b48ce9", - "syntaxPunctuation": "#ababab", - "thinkingOff": "comment", - "thinkingMinimal": "borderUi", - "thinkingLow": "#6ebbdf", - "thinkingMedium": "diagnostic", - "thinkingHigh": "mauve", - "thinkingXhigh": "coral", - "bashMode": "lucuma" - } -} diff --git a/DreamcoderProfiles/deploy/desktop-arch.json b/DreamcoderProfiles/deploy/desktop-arch.json index f62c9a9d..6bafbc4e 100644 --- a/DreamcoderProfiles/deploy/desktop-arch.json +++ b/DreamcoderProfiles/deploy/desktop-arch.json @@ -1,9 +1,9 @@ { "kind": "deployment", "name": "desktop-arch", - "description": "Desktop Arch Linux deployment of Dreamcoder with full day/night theme support. Repository-safe: contains no device address, username, key, or host runtime state.", + "description": "Desktop Arch Linux deployment of Dreamcoder with full light/dark theme support. Repository-safe: contains no device address, username, key, or host runtime state.", "platform": "arch-desktop", "os": "Arch Linux", "terminal_default_mode": "light", - "notes": "Day/night switching is handled by systemd timers via scripts/theme-auto.sh. Herdr [ui] pane_scrollbars follows the Herdr default (false); the mobile profile pins the same setting explicitly." + "notes": "Light/dark switching is handled by systemd timers via scripts/theme-auto.sh. Herdr [ui] pane_scrollbars follows the Herdr default (false); the mobile profile pins the same setting explicitly." } diff --git a/DreamcoderShell/.config/starship-night.toml b/DreamcoderShell/.config/starship-night.toml deleted file mode 100644 index 2363f9e9..00000000 --- a/DreamcoderShell/.config/starship-night.toml +++ /dev/null @@ -1,146 +0,0 @@ -# ======================================================== -# Dreamcoder Dark Night — Starship prompt -# ======================================================== -# Modern two-line layout with powerline segments. -# Line 1: context (directory, git), fill, cmd_duration, time -# Line 2: input character only (clean) -# Extra: status (exit code), AI session (hidden until active) - -add_newline = true -palette = "dreamcoder-night" -command_timeout = 500 - -format = """ -[\uE0B6](fg:prompt_surface0)\ -$username\ -[\uE0B0](bg:prompt_surface1 fg:prompt_surface0)\ -$directory\ -[\uE0B0](bg:prompt_accent fg:prompt_surface1)\ -$git_branch\ -$git_status\ -[\uE0B4](fg:prompt_accent)\ -$fill\ -$cmd_duration\ -$time -$character""" - -right_format = "" - -[palettes.dreamcoder-night] -bg = "#000000" -text = "#c6c6c6" -muted = "#ababab" -prompt_bg = "#000000" -prompt_surface0 = "#090909" -prompt_surface1 = "#0b0b0d" -prompt_surface2 = "#1b1b1b" -prompt_text = "#c6c6c6" -prompt_muted = "#ababab" -prompt_accent = "#a4afee" -prompt_accent_2 = "#b48ce9" -sage = "#43b48b" -success = "#43b48b" -diagnostic = "#62b9e2" -info = "#62b9e2" -lavender = "#b48ce9" -mauve = "#b98beb" -error = "#e88484" -warning = "#d1a126" -border = "#797979" -focus = "#3a76d6" -link = "#a4afee" - -[username] -show_always = true -style_user = "bg:prompt_surface0 fg:prompt_text bold" -style_root = "bg:prompt_surface0 fg:error bold" -format = "[  $user ]($style)" - -[directory] -style = "bg:prompt_surface1 fg:prompt_text bold" -format = "[ $path ]($style)" -truncation_length = 2 -truncate_to_repo = true -home_symbol = "" - -[git_branch] -symbol = "\uf418" -style = "bg:prompt_accent fg:prompt_bg bold" -format = "[ $symbol $branch ]($style)" - -[git_status] -style = "bg:prompt_accent fg:prompt_bg bold" -format = "[$all_status$ahead_behind ]($style)" -conflicted = "\ue727${count} " -ahead = "\u21E1${count} " -behind = "\u21E3${count} " -diverged = "\u2195\u21E1${ahead_count}\u21E3${behind_count} " -untracked = "?${count} " -stashed = "\uf0CF${count} " -modified = "~${count} " -staged = "+${count} " -renamed = "\u00BB${count} " -deleted = "\u2718${count} " - -[status] -disabled = false -format = "[$symbol]($style)" -symbol = "\u2717" -style = "bg:error fg:prompt_bg bold" -pipestatus = false - -[fill] -symbol = " " - -[cmd_duration] -min_time = 2500 -style = "fg:prompt_muted" -format = "[ \uf552 $duration ]($style)" - -[time] -disabled = false -format = "[ $time ]($style)" -style = "fg:prompt_muted" - -[character] -success_symbol = "[\u276F](bold fg:prompt_accent)" -error_symbol = "[\u276F](bold fg:error)" -vimcmd_symbol = "[\u276E](bold fg:sage)" - -# Runtime versions - show only when relevant, keep compact -[bun] -symbol = "\uF5EF" -style = "fg:prompt_accent bold" -format = "[ $symbol $version]($style)" - -[nodejs] -symbol = "\uE718" -style = "fg:sage bold" -format = "[ $symbol $version]($style)" - -[python] -symbol = "\uE73C" -style = "fg:diag bold" -format = "[ $symbol $version]($style)" - -[golang] -symbol = "\uE627" -style = "fg:diag bold" -format = "[ $symbol $version]($style)" - -[rust] -symbol = "\uE7A8" -style = "fg:mauve bold" -format = "[ $symbol $version]($style)" - -[custom.ai_session] -command = "cat ~/.cache/dreamcoder/ai-session.state 2>/dev/null || echo ''" -when = """test -f ~/.cache/dreamcoder/ai-session.state""" -format = "[\uf5B5 $output]($style)" -style = "fg:diag bold" - -[docker_context] -symbol = "\uf308" -style = "fg:diag bold" -format = "[ $symbol $context]($style)" -only_with_files = true diff --git a/DreamcoderSystemd/.config/systemd/user/dreamcoder-theme-auto.timer b/DreamcoderSystemd/.config/systemd/user/dreamcoder-theme-auto.timer index eabf8b45..c790383a 100644 --- a/DreamcoderSystemd/.config/systemd/user/dreamcoder-theme-auto.timer +++ b/DreamcoderSystemd/.config/systemd/user/dreamcoder-theme-auto.timer @@ -1,10 +1,9 @@ [Unit] -Description=Run Dreamcoder day/night theme automation +Description=Run Dreamcoder light/dark theme automation [Timer] OnBootSec=2min OnCalendar=*-*-* 07:00:00 -OnCalendar=*-*-* 16:00:00 OnCalendar=*-*-* 18:00:00 Persistent=true AccuracySec=1min diff --git a/DreamcoderThemes/dreamcoder/README.md b/DreamcoderThemes/dreamcoder/README.md index cca1b8ce..7e9c2707 100644 --- a/DreamcoderThemes/dreamcoder/README.md +++ b/DreamcoderThemes/dreamcoder/README.md @@ -6,7 +6,6 @@ This directory contains the Dreamcoder visual contract and generated color-only - `tokens.schema.json`: machine-readable token contract. - `*-dark.*`: Dreamcoder Dark mode with near-black bg, cool steel-blue accents, and muted slate surfaces. - `*-light.*`: paper-like daytime mode with flat surface ladder and distinct semantic tokens. -- `*-night.*`: Night/Dim rendering profile — deterministic brightness/saturation-reduced derivation of Dreamcoder Dark (user-controlled display profile, no medical claims). ## Color-only Snippets (Hooks) Hook these into your app configs after ML4W/Gentleman files: diff --git a/DreamcoderThemes/dreamcoder/bat-dreamcoder-night.sh b/DreamcoderThemes/dreamcoder/bat-dreamcoder-night.sh deleted file mode 100644 index 65f82c79..00000000 --- a/DreamcoderThemes/dreamcoder/bat-dreamcoder-night.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# Sourced snippet: never set shell options here. -# Dreamcoder Dark Night — Bat theme; run 'bat cache --build' after installing the tmTheme. -export BAT_THEME="Dreamcoder-Night" -export BAT_STYLE="auto,changes,header,grid" -export BAT_PAGER="less -RF" -export BAT_TABS="4" diff --git a/DreamcoderThemes/dreamcoder/btop-dreamcoder-night.theme b/DreamcoderThemes/dreamcoder/btop-dreamcoder-night.theme deleted file mode 100644 index d7cc6eb5..00000000 --- a/DreamcoderThemes/dreamcoder/btop-dreamcoder-night.theme +++ /dev/null @@ -1,64 +0,0 @@ -# ======================================================== -# Dreamcoder Dark Night — Btop theme -# ======================================================== -# Place in ~/.config/btop/themes/ and select from Btop UI. - -theme[main_bg]="#000000" -theme[main_fg]="#c6c6c6" -theme[title]="#a4afee" -theme[hi_fg]="#a4afee" -theme[selected_bg]="#0b0b0d" -theme[selected_fg]="#c6c6c6" -theme[inactive_fg]="#ababab" -theme[graph_line]="#5c5c5c" -theme[proc_misc]="#ababab" -theme[cpu_core]="#62b9e2" -theme[mem_free]="#43b48b" -theme[mem_used]="#b48ce9" -theme[mem_cached]="#62b9e2" -theme[user_bg]="#090909" -theme[user_fg]="#c6c6c6" -theme[temp]="#d1a126" -theme[disk]="#62b9e2" -theme[process]="#b48ce9" -theme[process_selected]="#a4afee" -theme[core_bar]="#a4afee" -theme[temp_bar]="#d1a126" -theme[swap]="#b98beb" -theme[div_line]="#5c5c5c" -theme[process_bg]="#090909" -theme[process_fg]="#c6c6c6" -theme[bad]="#e88484" -theme[good]="#43b48b" -theme[widget_bg]="#090909" -theme[widget_fg]="#c6c6c6" -theme[widget_border]="#5c5c5c" -theme[widget_selected]="#a4afee" -theme[graph_bg]="#090909" -theme[graph_fg]="#c6c6c6" -theme[graph_high]="#b48ce9" -theme[graph_low]="#62b9e2" -theme[graph_med]="#a4afee" -theme[proc_bg]="#090909" -theme[proc_fg]="#c6c6c6" -theme[process_border]="#5c5c5c" -theme[widget_title]="#a4afee" -theme[box_border]="#5c5c5c" -theme[box_bg]="#090909" -theme[box_fg]="#c6c6c6" -theme[box_selected]="#a4afee" -theme[os_bg]="#090909" -theme[os_fg]="#c6c6c6" -theme[clock_bg]="#090909" -theme[clock_fg]="#a4afee" -theme[bat_high]="#43b48b" -theme[bat_med]="#d1a126" -theme[bat_low]="#e88484" -theme[sensor_bg]="#090909" -theme[sensor_fg]="#c6c6c6" -theme[sensor_bar_bg]="#0b0b0d" -theme[sensor_bar_fg]="#a4afee" -theme[net_bg]="#090909" -theme[net_fg]="#c6c6c6" -theme[net_download]="#43b48b" -theme[net_upload]="#b48ce9" diff --git a/DreamcoderThemes/dreamcoder/cava-dreamcoder-night.config b/DreamcoderThemes/dreamcoder/cava-dreamcoder-night.config deleted file mode 100644 index c9876824..00000000 --- a/DreamcoderThemes/dreamcoder/cava-dreamcoder-night.config +++ /dev/null @@ -1,26 +0,0 @@ -# ======================================================== -# Dreamcoder Dark Night — Cava theme -# ======================================================== -# Include from ~/.config/cava/config or place in -# ~/.config/cava/dreamcoder-cava.config. - -[color] -# Background -background = '#000000' - -# Gradient mode for smooth transitions -gradient = 1 -gradient_count = 8 - -# Gradient colors (low to high frequency) — raw palette, muted at lower end -gradient_color_1 = '#274a5a' -gradient_color_2 = '#3b6f88' -gradient_color_3 = '#525877' -gradient_color_4 = '#737aa7' -gradient_color_5 = '#a4afee' -gradient_color_6 = '#b48ce9' -gradient_color_7 = '#a25c5c' -gradient_color_8 = '#e88484' - -# Mono color (used when gradient = 0) -foreground = '#a4afee' diff --git a/DreamcoderThemes/dreamcoder/delta-dreamcoder-night.gitconfig b/DreamcoderThemes/dreamcoder/delta-dreamcoder-night.gitconfig deleted file mode 100644 index b7b32ad1..00000000 --- a/DreamcoderThemes/dreamcoder/delta-dreamcoder-night.gitconfig +++ /dev/null @@ -1,45 +0,0 @@ -# ======================================================== -# Dreamcoder Dark Night — Git Delta theme -# ======================================================== -# Include from ~/.config/git/config: -# [include] -# path = ~/.config/git/delta-dreamcoder.gitconfig - -[delta] - # Syntax highlighting theme for diff content - syntax-theme = Dreamcoder-Night - - # Line colors - plus-color = "#2a6852" - minus-color = "#854e4e" - plus-emph-color = "#43b48b" - minus-emph-color = "#e88484" - - # Diff UI - file-style = "#a4afee" - file-decoration-style = "bold yellow box ul" - hunk-header-style = "file line-number syntax" - hunk-header-decoration-style = "yellow box" - hunk-header-file-style = "#a4afee" - hunk-header-line-number-style = "#ababab" - hunk-header-color = "#737374" - - # Commit decorations - commit-style = "#a4afee bold" - commit-decoration-style = "bold yellow box ul" - - # Line numbers - line-numbers = true - line-numbers-left-style = "#ababab" - line-numbers-right-style = "#ababab" - line-numbers-minus-style = "#e88484" - line-numbers-plus-style = "#43b48b" - - # Side-by-side - side-by-side = true - - # Whitespace highlighting - whitespace-error-style = "#d1a126" - - # Navigation - navigate = true diff --git a/DreamcoderThemes/dreamcoder/design-system.json b/DreamcoderThemes/dreamcoder/design-system.json index 73dc160a..73b67059 100644 --- a/DreamcoderThemes/dreamcoder/design-system.json +++ b/DreamcoderThemes/dreamcoder/design-system.json @@ -519,7 +519,7 @@ "source": "DreamcoderThemes/dreamcoder/tokens.json", "generator": "sync.py::sync_repo_snippets", "path": ".opencode/themes/dreamcoder.json", - "mode_policy": "base-modes+derived-night", + "mode_policy": "base-modes", "discovery_root": ".opencode/themes", "pattern": "*.json", "validation": "exact-bytes" diff --git a/DreamcoderThemes/dreamcoder/dunst-dreamcoder-night.conf b/DreamcoderThemes/dreamcoder/dunst-dreamcoder-night.conf deleted file mode 100644 index 97507f32..00000000 --- a/DreamcoderThemes/dreamcoder/dunst-dreamcoder-night.conf +++ /dev/null @@ -1,23 +0,0 @@ -# ======================================================== -# Dreamcoder Dark Night — Dunst theme -# ======================================================== -# Include from dunstrc: -# [include] dreamcoder-dunst.conf - -[urgency_low] - background = "#000000" - foreground = "#c6c6c6" - highlight = "#a4afee" - frame_color = "#5c5c5c" - -[urgency_normal] - background = "#090909" - foreground = "#c6c6c6" - highlight = "#a4afee" - frame_color = "#797979" - -[urgency_critical] - background = "#462828" - foreground = "#c6c6c6" - highlight = "#e88484" - frame_color = "#e88484" diff --git a/DreamcoderThemes/dreamcoder/firefox-dreamcoder-night.css b/DreamcoderThemes/dreamcoder/firefox-dreamcoder-night.css deleted file mode 100644 index c5390f5d..00000000 --- a/DreamcoderThemes/dreamcoder/firefox-dreamcoder-night.css +++ /dev/null @@ -1,138 +0,0 @@ -/* ======================================================== - Dreamcoder Dark Night — Firefox userChrome.css - ======================================================== - Place in ~/.mozilla/firefox/*.default-release/chrome/userChrome.css - Requires toolkit.legacyUserProfileCustomizations.stylesheets = true - in about:config. */ - -:root { - --dreamcoder-bg: #000000; - --dreamcoder-surface: #090909; - --dreamcoder-surface-1: #0b0b0d; - --dreamcoder-text: #c6c6c6; - --dreamcoder-text-dim: #ababab; - --dreamcoder-accent: #a4afee; - --dreamcoder-accent-2: #b48ce9; - --dreamcoder-border: #5c5c5c; - --dreamcoder-border-ui: #797979; - --dreamcoder-error: #e88484; - --dreamcoder-warning: #d1a126; - --dreamcoder-sage: #43b48b; - --dreamcoder-input-bg: #090909; - --dreamcoder-toolbar-bg: #090909; - --dreamcoder-hover: #191a24; - --dreamcoder-active: #292c3c; -} - -/* Main window */ -#main-window, -#navigator-toolbox { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text) !important; -} - -/* Toolbar & URL bar */ -#nav-bar, -#nav-bar toolbarbutton, -#urlbar, -#urlbar-background { - background-color: var(--dreamcoder-toolbar-bg) !important; - color: var(--dreamcoder-text) !important; - border-color: var(--dreamcoder-border) !important; -} - -#urlbar[focused="true"] > #urlbar-background { - border-color: var(--dreamcoder-accent) !important; -} - -/* Sidebar (bookmarks, history) */ -#sidebar-box, -#sidebar { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text) !important; -} - -.sidebar-placesTreechildren { - color: var(--dreamcoder-text) !important; -} - -/* Tab bar */ -#TabsToolbar, -#tabbrowser-tabs, -.tab-background { - background-color: var(--dreamcoder-bg) !important; -} - -.tabbrowser-tab:not([selected]) .tab-background { - background-color: var(--dreamcoder-surface) !important; -} - -.tabbrowser-tab[selected] .tab-background { - background-color: var(--dreamcoder-toolbar-bg) !important; - border-color: var(--dreamcoder-accent) !important; -} - -.tabbrowser-tab .tab-label { - color: var(--dreamcoder-text-dim) !important; -} - -.tabbrowser-tab[selected] .tab-label { - color: var(--dreamcoder-text) !important; -} - -/* Context menus */ -menupopup, -popup { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text) !important; -} - -menuitem { - color: var(--dreamcoder-text) !important; -} - -menuitem:hover { - background-color: var(--dreamcoder-hover) !important; - color: var(--dreamcoder-text) !important; -} - -/* Status panel */ -#statuspanel-label { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text-dim) !important; - border-color: var(--dreamcoder-border) !important; -} - -/* Find bar */ -#findbar { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text) !important; -} - -#findbar input { - background-color: var(--dreamcoder-input-bg) !important; - color: var(--dreamcoder-text) !important; - border-color: var(--dreamcoder-border) !important; -} - -/* Private browsing indicators */ -#private-browsing-indicator-with-label { - color: var(--dreamcoder-accent-2) !important; -} - -/* Downloads panel */ -#downloadsPanel, -#downloadsListBox { - background-color: var(--dreamcoder-bg) !important; - color: var(--dreamcoder-text) !important; -} - -/* Scrollbar styling */ -:root { - scrollbar-color: var(--dreamcoder-border) var(--dreamcoder-bg) !important; -} - -* { - scrollbar-width: thin !important; - scrollbar-color: var(--dreamcoder-border) var(--dreamcoder-bg) !important; -} diff --git a/DreamcoderThemes/dreamcoder/fzf-dreamcoder-night.sh b/DreamcoderThemes/dreamcoder/fzf-dreamcoder-night.sh deleted file mode 100644 index 30b55ca8..00000000 --- a/DreamcoderThemes/dreamcoder/fzf-dreamcoder-night.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -# Sourced snippet: never set shell options here. -# Dreamcoder Dark Night — fzf -export FZF_DEFAULT_OPTS="${FZF_DEFAULT_OPTS:-} --color=bg:#000000,bg+:#0b0b0d,fg:#c6c6c6,fg+:#c6c6c6,hl:#a4afee,hl+:#a4afee,info:#62b9e2,marker:#43b48b,prompt:#a4afee,spinner:#b48ce9,pointer:#b48ce9,header:#ababab,border:#5c5c5c,label:#ababab,query:#c6c6c6,gutter:#0b0b0d,scrollbar:#5c5c5c,separator:#5c5c5c,preview-bg:#0b0b0d,preview-border:#5c5c5c" diff --git a/DreamcoderThemes/dreamcoder/hypr-colors-night.conf b/DreamcoderThemes/dreamcoder/hypr-colors-night.conf deleted file mode 100644 index 47cac532..00000000 --- a/DreamcoderThemes/dreamcoder/hypr-colors-night.conf +++ /dev/null @@ -1,50 +0,0 @@ -$background = rgba(000000ff) -$error = rgba(e88484ff) -$error_container = rgba(090909ff) -$inverse_on_surface = rgba(090909ff) -$inverse_primary = rgba(a4afeeff) -$inverse_surface = rgba(c6c6c6ff) -$on_background = rgba(c6c6c6ff) -$on_error = rgba(000000ff) -$on_error_container = rgba(c6c6c6ff) -$on_primary = rgba(000000ff) -$on_primary_container = rgba(c6c6c6ff) -$on_primary_fixed = rgba(000000ff) -$on_primary_fixed_variant = rgba(a4afeeff) -$on_secondary = rgba(000000ff) -$on_secondary_container = rgba(c6c6c6ff) -$on_secondary_fixed = rgba(000000ff) -$on_secondary_fixed_variant = rgba(b48ce9ff) -$on_surface = rgba(c6c6c6ff) -$on_surface_variant = rgba(abababff) -$on_tertiary = rgba(000000ff) -$on_tertiary_container = rgba(c6c6c6ff) -$on_tertiary_fixed = rgba(000000ff) -$on_tertiary_fixed_variant = rgba(3a76d6ff) -$outline = rgba(5c5c5cff) -$outline_variant = rgba(797979ff) -$primary = rgba(a4afeeff) -$primary_container = rgba(0b0b0dff) -$primary_fixed = rgba(a4afeeff) -$primary_fixed_dim = rgba(b48ce9ff) -$scrim = rgba(000000ff) -$secondary = rgba(b48ce9ff) -$secondary_container = rgba(0b0b0dff) -$secondary_fixed = rgba(b48ce9ff) -$secondary_fixed_dim = rgba(b48ce9ff) -$shadow = rgba(000000ff) -$source_color = rgba(a4afeeff) -$surface = rgba(000000ff) -$surface_bright = rgba(090909ff) -$surface_container = rgba(0b0b0dff) -$surface_container_high = rgba(0b0b0dff) -$surface_container_highest = rgba(1b1b1bff) -$surface_container_low = rgba(090909ff) -$surface_container_lowest = rgba(090909ff) -$surface_dim = rgba(090909ff) -$surface_tint = rgba(a4afeeff) -$surface_variant = rgba(0b0b0dff) -$tertiary = rgba(62b9e2ff) -$tertiary_container = rgba(090909ff) -$tertiary_fixed = rgba(62b9e2ff) -$tertiary_fixed_dim = rgba(62b9e2ff) diff --git a/DreamcoderThemes/dreamcoder/hypr-colors-night.lua b/DreamcoderThemes/dreamcoder/hypr-colors-night.lua deleted file mode 100644 index 82f07c29..00000000 --- a/DreamcoderThemes/dreamcoder/hypr-colors-night.lua +++ /dev/null @@ -1,50 +0,0 @@ -background = "rgba(000000ff)" -error = "rgba(e88484ff)" -error_container = "rgba(090909ff)" -inverse_on_surface = "rgba(090909ff)" -inverse_primary = "rgba(a4afeeff)" -inverse_surface = "rgba(c6c6c6ff)" -on_background = "rgba(c6c6c6ff)" -on_error = "rgba(000000ff)" -on_error_container = "rgba(c6c6c6ff)" -on_primary = "rgba(000000ff)" -on_primary_container = "rgba(c6c6c6ff)" -on_primary_fixed = "rgba(000000ff)" -on_primary_fixed_variant = "rgba(a4afeeff)" -on_secondary = "rgba(000000ff)" -on_secondary_container = "rgba(c6c6c6ff)" -on_secondary_fixed = "rgba(000000ff)" -on_secondary_fixed_variant = "rgba(b48ce9ff)" -on_surface = "rgba(c6c6c6ff)" -on_surface_variant = "rgba(abababff)" -on_tertiary = "rgba(000000ff)" -on_tertiary_container = "rgba(c6c6c6ff)" -on_tertiary_fixed = "rgba(000000ff)" -on_tertiary_fixed_variant = "rgba(3a76d6ff)" -outline = "rgba(5c5c5cff)" -outline_variant = "rgba(797979ff)" -primary = "rgba(a4afeeff)" -primary_container = "rgba(0b0b0dff)" -primary_fixed = "rgba(a4afeeff)" -primary_fixed_dim = "rgba(b48ce9ff)" -scrim = "rgba(000000ff)" -secondary = "rgba(b48ce9ff)" -secondary_container = "rgba(0b0b0dff)" -secondary_fixed = "rgba(b48ce9ff)" -secondary_fixed_dim = "rgba(b48ce9ff)" -shadow = "rgba(000000ff)" -source_color = "rgba(a4afeeff)" -surface = "rgba(000000ff)" -surface_bright = "rgba(090909ff)" -surface_container = "rgba(0b0b0dff)" -surface_container_high = "rgba(0b0b0dff)" -surface_container_highest = "rgba(1b1b1bff)" -surface_container_low = "rgba(090909ff)" -surface_container_lowest = "rgba(090909ff)" -surface_dim = "rgba(090909ff)" -surface_tint = "rgba(a4afeeff)" -surface_variant = "rgba(0b0b0dff)" -tertiary = "rgba(62b9e2ff)" -tertiary_container = "rgba(090909ff)" -tertiary_fixed = "rgba(62b9e2ff)" -tertiary_fixed_dim = "rgba(62b9e2ff)" diff --git a/DreamcoderThemes/dreamcoder/hyprland-night.conf b/DreamcoderThemes/dreamcoder/hyprland-night.conf deleted file mode 100644 index 60e35c32..00000000 --- a/DreamcoderThemes/dreamcoder/hyprland-night.conf +++ /dev/null @@ -1,11 +0,0 @@ -# Dreamcoder color layer for Hyprland. -# Import after ML4W/Gentleman defaults; this changes colors only. - -general { - col.active_border = rgba(c6c6c6ff) rgba(abababff) 45deg - col.inactive_border = rgba(282828d6) -} - -misc { - background_color = rgba(000000ff) -} diff --git a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh deleted file mode 100644 index 01a82b8b..00000000 --- a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Sourced snippet: never set shell options here. -# Dreamcoder Dark Night — LS_COLORS / eza -export LS_COLORS='di=38;2;164;175;238:ex=38;2;180;140;233:ln=38;2;98;185;226:or=48;2;209;161;38;38;2;0;0;0:so=38;2;67;180;139:pi=38;2;209;161;38:bd=38;2;232;132;132:cd=38;2;232;132;132:su=48;2;0;0;0;38;2;180;140;233:sg=48;2;0;0;0;38;2;180;140;233:tw=38;2;164;175;238;48;2;9;9;9:ow=38;2;164;175;238;48;2;9;9;9:st=38;2;164;175;238;48;2;9;9;9:*.sh=38;2;180;140;233:*.bash=38;2;180;140;233:*.zsh=38;2;180;140;233:*.fish=38;2;180;140;233:*.py=38;2;180;140;233:*.rb=38;2;180;140;233:*.rs=38;2;180;140;233:*.go=38;2;180;140;233:*.ts=38;2;180;140;233:*.js=38;2;180;140;233:*.jpg=38;2;185;139;235:*.jpeg=38;2;185;139;235:*.png=38;2;185;139;235:*.gif=38;2;185;139;235:*.bmp=38;2;185;139;235:*.svg=38;2;185;139;235:*.webp=38;2;185;139;235:*.ico=38;2;185;139;235:*.mp3=38;2;185;139;235:*.wav=38;2;185;139;235:*.flac=38;2;185;139;235:*.ogg=38;2;185;139;235:*.m4a=38;2;185;139;235:*.mp4=38;2;185;139;235:*.mkv=38;2;185;139;235:*.webm=38;2;185;139;235:*.mov=38;2;185;139;235:*.pdf=38;2;171;171;171:*.doc=38;2;171;171;171:*.docx=38;2;171;171;171:*.odt=38;2;171;171;171:*.xls=38;2;171;171;171:*.xlsx=38;2;171;171;171:*.ppt=38;2;171;171;171:*.pptx=38;2;171;171;171:*.txt=38;2;171;171;171:*.cfg=38;2;171;171;171:*.conf=38;2;171;171;171:*.json=38;2;171;171;171:*.yaml=38;2;171;171;171:*.yml=38;2;171;171;171:*.toml=38;2;171;171;171:*.xml=38;2;171;171;171:*.css=38;2;171;171;171:*.html=38;2;171;171;171:*.c=38;2;171;171;171:*.h=38;2;171;171;171:*.cpp=38;2;171;171;171:*.hpp=38;2;171;171;171:*.swp=38;2;171;171;171:*.swo=38;2;171;171;171:*.bak=38;2;171;171;171:*.orig=38;2;171;171;171:*.md=#a4afee' -export EZA_COLORS='di=38;2;164;175;238:ex=38;2;180;140;233:ln=38;2;98;185;226:so=38;2;67;180;139:pi=38;2;209;161;38:bd=38;2;232;132;132:cd=38;2;232;132;132:uw=38;2;209;161;38:ux=38;2;232;132;132:gwx=38;2;209;161;38:*.md=#a4afee' diff --git a/DreamcoderThemes/dreamcoder/obsidian-dreamcoder-night.css b/DreamcoderThemes/dreamcoder/obsidian-dreamcoder-night.css deleted file mode 100644 index 9c254f43..00000000 --- a/DreamcoderThemes/dreamcoder/obsidian-dreamcoder-night.css +++ /dev/null @@ -1,143 +0,0 @@ -/* ======================================================== - Dreamcoder Dark Night — Obsidian CSS snippet - ======================================================== - Place in your vault's .obsidian/snippets/ folder. - Enable in Settings → Appearance → CSS snippets. */ - -.theme-dark { - /* Base — raw palette colors for backgrounds and surfaces */ - --background-primary: #000000; - --background-primary-alt: #090909; - --background-secondary: #090909; - --background-secondary-alt: #0b0b0d; - --background-modifier-border: #5c5c5c; - --background-modifier-border-hover: #797979; - --background-modifier-form-field: #090909; - --background-modifier-success: #112d23; - --background-modifier-error: #3a2121; - --background-modifier-message: #0b0b0d; - - /* Text — guarded against background for accessibility */ - --text-normal: #c6c6c6; - --text-muted: #ababab; - --text-faint: #959595; - --text-accent: #a4afee; - --text-accent-hover: #b48ce9; - --text-error: #e88484; - --text-warning: #d1a126; - --text-success: #43b48b; - --text-selection: #323232; - --text-on-accent: #000000; - - /* Interactive */ - --interactive-normal: #0b0b0d; - --interactive-hover: #323232; - --interactive-accent: #a4afee; - --interactive-accent-hover: #b48ce9; - --interactive-success: #43b48b; - - /* Scrollbar */ - --scrollbar-bg: transparent; - --scrollbar-thumb-bg: #5c5c5c; - --scrollbar-active-thumb-bg: #797979; - - /* Code — syntax highlighting colors */ - --code-normal: #c6c6c6; - --code-comment: #b3b3b3; - --code-punctuation: #ababab; - --code-keyword: #a4afee; - --code-operator: #b48ce9; - --code-function: #b48ce9; - --code-string: #43b48b; - --code-number: #b98beb; - --code-tag: #a4afee; - --code-important: #e88484; - --code-background: #090909; - - /* Heading */ - --h1-color: #d3d3d3; - --h2-color: #d3d3d3; - --h3-color: #b48ce9; - --h4-color: #62b9e2; - --h5-color: #ababab; - --h6-color: #959595; - - /* Link */ - --link-color: #a4afee; - --link-color-hover: #b48ce9; - --link-external-color: #62b9e2; - --link-external-color-hover: #b48ce9; - - /* Checkbox */ - --checkbox-color: #a4afee; - --checkbox-color-hover: #b48ce9; - --checkbox-border-color: #5c5c5c; - --checkbox-mark-color: #000000; - - /* Table */ - --table-header-background: #0b0b0d; - --table-header-background-hover: #1b1b1b; - --table-row-background-hover: #090909; - --table-border-color: #5c5c5c; - - /* Graph */ - --graph-line: #5c5c5c; - --graph-node: #ababab; - --graph-node-focused: #a4afee; - --graph-node-tag: #62b9e2; - --graph-node-attachment: #43b48b; -} - -/* Headings */ -.markdown-rendered h1 { color: var(--h1-color); } -.markdown-rendered h2 { color: var(--h2-color); } -.markdown-rendered h3 { color: var(--h3-color); } -.markdown-rendered h4 { color: var(--h4-color); } -.markdown-rendered h5 { color: var(--h5-color); } -.markdown-rendered h6 { color: var(--h6-color); } - -/* Tags */ -.tag { - background-color: #191a24; - color: #a4afee; - border-radius: 4px; - padding: 0 6px; -} - -/* Blockquotes */ -blockquote { - border-color: #a4afee !important; -} - -/* Search highlights */ -.search-result-file-matched-text, -.is-selected .search-result-file-matched-text, -mark { - background-color: #212330 !important; - color: #c6c6c6 !important; -} - -/* Active line in edit mode */ -.cm-active { - background-color: #090909 !important; -} - -/* Selection */ -::selection { - background-color: #060607 !important; -} - -/* Tooltip */ -.tooltip { - background-color: #0b0b0d !important; - color: #c6c6c6 !important; -} - -/* Menu */ -.menu { - background-color: #090909 !important; -} - -.menu-item:hover { - background-color: #0b0b0d !important; -} diff --git a/DreamcoderThemes/dreamcoder/rofi-night.rasi b/DreamcoderThemes/dreamcoder/rofi-night.rasi deleted file mode 100644 index 466f85fd..00000000 --- a/DreamcoderThemes/dreamcoder/rofi-night.rasi +++ /dev/null @@ -1,38 +0,0 @@ -/* Dreamcoder color layer for Rofi. */ -* { - background: rgba(11, 11, 13, 0.92); - background-alt: #090909; - foreground: #c6c6c6; - muted: #ababab; - selected: rgba(81, 84, 212, 0.28); - active: #a4afee; - border-ui: #797979; - focus: #3a76d6; - urgent: #e88484; - border-color: #797979; -} - -window { - background-color: @background; - border: 1px; - border-color: @border-ui; - border-radius: 18px; -} - -entry { - background-color: @selected; - text-color: @foreground; - border: 1px; - border-color: @focus; - border-radius: 12px; -} - -element selected { - background-color: @selected; - text-color: @foreground; - border: 0 0 0 3px; - border-color: @focus; -} - -element-text { text-color: @muted; } -element selected element-text { text-color: @foreground; } diff --git a/DreamcoderThemes/dreamcoder/targets.json b/DreamcoderThemes/dreamcoder/targets.json index 79b9a6ca..260004d2 100644 --- a/DreamcoderThemes/dreamcoder/targets.json +++ b/DreamcoderThemes/dreamcoder/targets.json @@ -2,42 +2,42 @@ "$schema":"./targets.schema.json", "schema":"dreamcoder.theme-targets.v1", "targets":[ - {"id":"antigravity","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-antigravity","destinations":["$XDG_CONFIG_HOME/dreamcoder/antigravity"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/antigravity","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"antigravity","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/antigravity-dark","light":"DreamcoderThemes/dreamcoder/generated/antigravity-light"}}}, - {"id":"bat","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-bat","destinations":["$XDG_CONFIG_HOME/dreamcoder/bat"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/bat","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"bat","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/bat-dark","light":"DreamcoderThemes/dreamcoder/generated/bat-light"}}}, - {"id":"btop","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-btop","destinations":["$XDG_CONFIG_HOME/dreamcoder/btop"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/btop","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"btop","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/btop-dark","light":"DreamcoderThemes/dreamcoder/generated/btop-light"}}}, - {"id":"cava","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-cava","destinations":["$XDG_CONFIG_HOME/dreamcoder/cava"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/cava","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"cava","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/cava-dark","light":"DreamcoderThemes/dreamcoder/generated/cava-light"}}}, - {"id":"codex-cli-settings","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-codex-cli-settings","destinations":["$XDG_CONFIG_HOME/dreamcoder/codex-cli-settings"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/codex-cli-settings","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"codex_cli_settings","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/codex-cli-settings-dark","light":"DreamcoderThemes/dreamcoder/generated/codex-cli-settings-light"}}}, - {"id":"codex-textmate","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-codex-textmate","destinations":["$XDG_CONFIG_HOME/dreamcoder/codex-textmate"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/codex-textmate","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"codex_textmate","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/codex-textmate-dark","light":"DreamcoderThemes/dreamcoder/generated/codex-textmate-light"}}}, - {"id":"delta","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-delta","destinations":["$XDG_CONFIG_HOME/dreamcoder/delta"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/delta","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"delta","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/delta-dark","light":"DreamcoderThemes/dreamcoder/generated/delta-light"}}}, + {"id":"antigravity","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-antigravity","destinations":["$XDG_CONFIG_HOME/dreamcoder/antigravity"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/antigravity","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"antigravity","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/antigravity-dark","light":"DreamcoderThemes/dreamcoder/generated/antigravity-light"}}}, + {"id":"bat","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-bat","destinations":["$XDG_CONFIG_HOME/dreamcoder/bat"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/bat","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"bat","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/bat-dark","light":"DreamcoderThemes/dreamcoder/generated/bat-light"}}}, + {"id":"btop","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-btop","destinations":["$XDG_CONFIG_HOME/dreamcoder/btop"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/btop","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"btop","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/btop-dark","light":"DreamcoderThemes/dreamcoder/generated/btop-light"}}}, + {"id":"cava","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-cava","destinations":["$XDG_CONFIG_HOME/dreamcoder/cava"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/cava","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"cava","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/cava-dark","light":"DreamcoderThemes/dreamcoder/generated/cava-light"}}}, + {"id":"codex-cli-settings","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-codex-cli-settings","destinations":["$XDG_CONFIG_HOME/dreamcoder/codex-cli-settings"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/codex-cli-settings","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"codex_cli_settings","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/codex-cli-settings-dark","light":"DreamcoderThemes/dreamcoder/generated/codex-cli-settings-light"}}}, + {"id":"codex-textmate","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-codex-textmate","destinations":["$XDG_CONFIG_HOME/dreamcoder/codex-textmate"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/codex-textmate","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"codex_textmate","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/codex-textmate-dark","light":"DreamcoderThemes/dreamcoder/generated/codex-textmate-light"}}}, + {"id":"delta","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-delta","destinations":["$XDG_CONFIG_HOME/dreamcoder/delta"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/delta","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"delta","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/delta-dark","light":"DreamcoderThemes/dreamcoder/generated/delta-light"}}}, {"id":"doctor-maintenance","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-doctor-maintenance","destinations":["$XDG_CONFIG_HOME/dreamcoder/doctor-maintenance"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/doctor-maintenance","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true}, - {"id":"dunst","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-dunst","destinations":["$XDG_CONFIG_HOME/dreamcoder/dunst"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/dunst","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"dunst","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/dunst-dark","light":"DreamcoderThemes/dreamcoder/generated/dunst-light"}}}, + {"id":"dunst","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-dunst","destinations":["$XDG_CONFIG_HOME/dreamcoder/dunst"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/dunst","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"dunst","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/dunst-dark","light":"DreamcoderThemes/dreamcoder/generated/dunst-light"}}}, {"id":"dusk-runtime","classification":"excluded","reason":"Runtime activation is prohibited; dusk remains design-system-only.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-dusk-runtime","destinations":["$XDG_CONFIG_HOME/dreamcoder/dusk-runtime"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/dusk-runtime","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true,"excluded_behavior":"reject-before-mutation"}, - {"id":"firefox","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-firefox","destinations":["$XDG_CONFIG_HOME/dreamcoder/firefox"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/firefox","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"firefox","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/firefox-dark","light":"DreamcoderThemes/dreamcoder/generated/firefox-light"}}}, - {"id":"fzf","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-fzf","destinations":["$XDG_CONFIG_HOME/dreamcoder/fzf"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/fzf","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"fzf","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/fzf-dark","light":"DreamcoderThemes/dreamcoder/generated/fzf-light"}}}, + {"id":"firefox","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-firefox","destinations":["$XDG_CONFIG_HOME/dreamcoder/firefox"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/firefox","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"firefox","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/firefox-dark","light":"DreamcoderThemes/dreamcoder/generated/firefox-light"}}}, + {"id":"fzf","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-fzf","destinations":["$XDG_CONFIG_HOME/dreamcoder/fzf"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/fzf","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"fzf","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/fzf-dark","light":"DreamcoderThemes/dreamcoder/generated/fzf-light"}}}, {"id":"generated-repository-contract","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-generated-repository-contract","destinations":["$XDG_CONFIG_HOME/dreamcoder/generated-repository-contract"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/generated-repository-contract","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true}, {"id":"gentleman-module-plan","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-gentleman-module-plan","destinations":["$XDG_CONFIG_HOME/dreamcoder/gentleman-module-plan"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/gentleman-module-plan","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true}, - {"id":"ghostty","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-ghostty","destinations":["$XDG_CONFIG_HOME/dreamcoder/ghostty"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ghostty","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"ghostty","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/ghostty-dark","light":"DreamcoderThemes/dreamcoder/generated/ghostty-light"}}}, + {"id":"ghostty","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-ghostty","destinations":["$XDG_CONFIG_HOME/dreamcoder/ghostty"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ghostty","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"ghostty","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/ghostty-dark","light":"DreamcoderThemes/dreamcoder/generated/ghostty-light"}}}, {"id":"herdr","classification":"excluded","reason":"gated: unsupported-contract until version-bound schema and reload evidence exists.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-herdr","destinations":["$XDG_CONFIG_HOME/dreamcoder/herdr"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/herdr","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true,"excluded_behavior":"report-unsupported-contract"}, - {"id":"hyprland-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-hyprland-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/hyprland-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/hyprland-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"hyprland_colors","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/hyprland-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/hyprland-colors-light"}}}, - {"id":"kitty","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-kitty","destinations":["$XDG_CONFIG_HOME/dreamcoder/kitty"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/kitty","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"kitty","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/kitty-dark","light":"DreamcoderThemes/dreamcoder/generated/kitty-light"}}}, - {"id":"ls-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-ls-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/ls-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ls-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"ls_colors","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/ls-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/ls-colors-light"}}}, + {"id":"hyprland-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-hyprland-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/hyprland-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/hyprland-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"hyprland_colors","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/hyprland-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/hyprland-colors-light"}}}, + {"id":"kitty","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-kitty","destinations":["$XDG_CONFIG_HOME/dreamcoder/kitty"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/kitty","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"kitty","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/kitty-dark","light":"DreamcoderThemes/dreamcoder/generated/kitty-light"}}}, + {"id":"ls-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-ls-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/ls-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ls-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"ls_colors","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/ls-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/ls-colors-light"}}}, {"id":"ml4w-hook-plan","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-ml4w-hook-plan","destinations":["$XDG_CONFIG_HOME/dreamcoder/ml4w-hook-plan"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ml4w-hook-plan","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true}, {"id":"ml4w-structure","classification":"excluded","reason":"ML4W layout and lifecycle remain outside Dreamcoder ownership.","owner":"ml4w","availability":{"kind":"command"},"install":{"module":"Dreamcoder-ml4w-structure","destinations":["$XDG_CONFIG_HOME/dreamcoder/ml4w-structure"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/ml4w-structure","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true,"excluded_behavior":"report-ml4w-owned"}, - {"id":"neovim","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-neovim","destinations":["$XDG_CONFIG_HOME/dreamcoder/neovim"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/neovim","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"neovim","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/neovim-dark","light":"DreamcoderThemes/dreamcoder/generated/neovim-light"}}}, - {"id":"obsidian","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-obsidian","destinations":["$XDG_CONFIG_HOME/dreamcoder/obsidian"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/obsidian","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"obsidian","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/obsidian-dark","light":"DreamcoderThemes/dreamcoder/generated/obsidian-light"}}}, - {"id":"opencode-theme","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-opencode-theme","destinations":["$XDG_CONFIG_HOME/dreamcoder/opencode-theme"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/opencode-theme","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"opencode_theme","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/opencode-theme-dark","light":"DreamcoderThemes/dreamcoder/generated/opencode-theme-light"}}}, - {"id":"opencode-tui-selection","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-opencode-tui-selection","destinations":["$XDG_CONFIG_HOME/dreamcoder/opencode-tui-selection"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/opencode-tui-selection","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"opencode_tui_selection","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/opencode-tui-selection-dark","light":"DreamcoderThemes/dreamcoder/generated/opencode-tui-selection-light"}}}, - {"id":"pi","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-pi","destinations":["$XDG_CONFIG_HOME/dreamcoder/pi"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/pi","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"pi","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/pi-dark","light":"DreamcoderThemes/dreamcoder/generated/pi-light"}}}, - {"id":"rofi-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-rofi-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/rofi-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/rofi-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"rofi_colors","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/rofi-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/rofi-colors-light"}}}, - {"id":"shell-syntax","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-shell-syntax","destinations":["$XDG_CONFIG_HOME/dreamcoder/shell-syntax"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/shell-syntax","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"shell_syntax","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/shell-syntax-dark","light":"DreamcoderThemes/dreamcoder/generated/shell-syntax-light"}}}, - {"id":"starship","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-starship","destinations":["$XDG_CONFIG_HOME/dreamcoder/starship"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/starship","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"starship","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/starship-dark","light":"DreamcoderThemes/dreamcoder/generated/starship-light"}}}, + {"id":"neovim","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-neovim","destinations":["$XDG_CONFIG_HOME/dreamcoder/neovim"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/neovim","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"neovim","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/neovim-dark","light":"DreamcoderThemes/dreamcoder/generated/neovim-light"}}}, + {"id":"obsidian","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-obsidian","destinations":["$XDG_CONFIG_HOME/dreamcoder/obsidian"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/obsidian","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"obsidian","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/obsidian-dark","light":"DreamcoderThemes/dreamcoder/generated/obsidian-light"}}}, + {"id":"opencode-theme","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-opencode-theme","destinations":["$XDG_CONFIG_HOME/dreamcoder/opencode-theme"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/opencode-theme","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"opencode_theme","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/opencode-theme-dark","light":"DreamcoderThemes/dreamcoder/generated/opencode-theme-light"}}}, + {"id":"opencode-tui-selection","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-opencode-tui-selection","destinations":["$XDG_CONFIG_HOME/dreamcoder/opencode-tui-selection"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/opencode-tui-selection","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"opencode_tui_selection","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/opencode-tui-selection-dark","light":"DreamcoderThemes/dreamcoder/generated/opencode-tui-selection-light"}}}, + {"id":"pi","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-pi","destinations":["$XDG_CONFIG_HOME/dreamcoder/pi"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/pi","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"pi","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/pi-dark","light":"DreamcoderThemes/dreamcoder/generated/pi-light"}}}, + {"id":"rofi-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-rofi-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/rofi-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/rofi-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"rofi_colors","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/rofi-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/rofi-colors-light"}}}, + {"id":"shell-syntax","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-shell-syntax","destinations":["$XDG_CONFIG_HOME/dreamcoder/shell-syntax"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/shell-syntax","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"shell_syntax","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/shell-syntax-dark","light":"DreamcoderThemes/dreamcoder/generated/shell-syntax-light"}}}, + {"id":"starship","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-starship","destinations":["$XDG_CONFIG_HOME/dreamcoder/starship"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/starship","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"starship","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/starship-dark","light":"DreamcoderThemes/dreamcoder/generated/starship-light"}}}, {"id":"theme-scheduler","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-theme-scheduler","destinations":["$XDG_CONFIG_HOME/dreamcoder/theme-scheduler"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/theme-scheduler","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true}, - {"id":"tmux","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-tmux","destinations":["$XDG_CONFIG_HOME/dreamcoder/tmux"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/tmux","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"tmux","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/tmux-dark","light":"DreamcoderThemes/dreamcoder/generated/tmux-light"}}}, - {"id":"tmux-kanagawa-bridge","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-tmux-kanagawa-bridge","destinations":["$XDG_CONFIG_HOME/dreamcoder/tmux-kanagawa-bridge"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/tmux-kanagawa-bridge","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"tmux_kanagawa_bridge","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/tmux-kanagawa-bridge-dark","light":"DreamcoderThemes/dreamcoder/generated/tmux-kanagawa-bridge-light"}}}, + {"id":"tmux","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-tmux","destinations":["$XDG_CONFIG_HOME/dreamcoder/tmux"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/tmux","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"tmux","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/tmux-dark","light":"DreamcoderThemes/dreamcoder/generated/tmux-light"}}}, + {"id":"tmux-kanagawa-bridge","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-tmux-kanagawa-bridge","destinations":["$XDG_CONFIG_HOME/dreamcoder/tmux-kanagawa-bridge"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/tmux-kanagawa-bridge","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"tmux_kanagawa_bridge","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/tmux-kanagawa-bridge-dark","light":"DreamcoderThemes/dreamcoder/generated/tmux-kanagawa-bridge-light"}}}, {"id":"unrelated-application-settings","classification":"excluded","reason":"Non-theme application settings are not rollout artifacts.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-unrelated-application-settings","destinations":["$XDG_CONFIG_HOME/dreamcoder/unrelated-application-settings"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/unrelated-application-settings","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"selector_only":true,"excluded_behavior":"report-out-of-scope"}, - {"id":"wallpaper-matugen-hook","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-wallpaper-matugen-hook","destinations":["$XDG_CONFIG_HOME/dreamcoder/wallpaper-matugen-hook"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/wallpaper-matugen-hook","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"wallpaper_matugen_hook","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/wallpaper-matugen-hook-dark","light":"DreamcoderThemes/dreamcoder/generated/wallpaper-matugen-hook-light"}}}, - {"id":"warp","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-warp","destinations":["$XDG_CONFIG_HOME/dreamcoder/warp"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/warp","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"warp","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/warp-dark","light":"DreamcoderThemes/dreamcoder/generated/warp-light"}}}, - {"id":"waybar-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-waybar-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/waybar-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/waybar-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"waybar_colors","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/waybar-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/waybar-colors-light"}}}, - {"id":"zellij","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-zellij","destinations":["$XDG_CONFIG_HOME/dreamcoder/zellij"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/zellij","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"zellij","modes":["dark","light","night"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/zellij-dark","light":"DreamcoderThemes/dreamcoder/generated/zellij-light"}}} + {"id":"wallpaper-matugen-hook","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-wallpaper-matugen-hook","destinations":["$XDG_CONFIG_HOME/dreamcoder/wallpaper-matugen-hook"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/wallpaper-matugen-hook","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"wallpaper_matugen_hook","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/wallpaper-matugen-hook-dark","light":"DreamcoderThemes/dreamcoder/generated/wallpaper-matugen-hook-light"}}}, + {"id":"warp","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-warp","destinations":["$XDG_CONFIG_HOME/dreamcoder/warp"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/warp","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"warp","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/warp-dark","light":"DreamcoderThemes/dreamcoder/generated/warp-light"}}}, + {"id":"waybar-colors","classification":"required","reason":"Core Dreamcoder rollout contract.","owner":"dreamcoder","availability":{"kind":"always"},"install":{"module":"Dreamcoder-waybar-colors","destinations":["$XDG_CONFIG_HOME/dreamcoder/waybar-colors"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/waybar-colors","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"waybar_colors","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/waybar-colors-dark","light":"DreamcoderThemes/dreamcoder/generated/waybar-colors-light"}}}, + {"id":"zellij","classification":"optional","reason":"External integration; unavailable installations receive actionable reporting.","owner":"dreamcoder","availability":{"kind":"command"},"install":{"module":"Dreamcoder-zellij","destinations":["$XDG_CONFIG_HOME/dreamcoder/zellij"],"allowed_ownership":["missing","managed"]},"activation":{"selector":"managed-selectors/zellij","validate":{"kind":"anchor"},"reload":{"kind":"none","observable":false},"rollback":"selector"},"version":{"constraint":null},"render":{"renderer":"zellij","modes":["dark","light"],"repository_outputs":{"dark":"DreamcoderThemes/dreamcoder/generated/zellij-dark","light":"DreamcoderThemes/dreamcoder/generated/zellij-light"}}} ] } diff --git a/DreamcoderThemes/dreamcoder/targets.schema.json b/DreamcoderThemes/dreamcoder/targets.schema.json index b003b159..89cb3d4f 100644 --- a/DreamcoderThemes/dreamcoder/targets.schema.json +++ b/DreamcoderThemes/dreamcoder/targets.schema.json @@ -1 +1 @@ -{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","required":["$schema","schema","targets"],"properties":{"$schema":{"const":"./targets.schema.json"},"schema":{"const":"dreamcoder.theme-targets.v1"},"targets":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/target"}}},"additionalProperties":false,"$defs":{"target":{"type":"object","required":["id","classification","reason","owner","availability","install","activation","version"],"properties":{"id":{"type":"string","minLength":1},"classification":{"enum":["required","optional","excluded"]},"reason":{"type":"string","minLength":1},"owner":{"type":"string","minLength":1},"availability":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string"}},"additionalProperties":false},"render":{"type":"object","required":["renderer","modes","repository_outputs"],"properties":{"renderer":{"type":"string"},"modes":{"type":"array","minItems":1,"items":{"type":"string","description":"Runtime render mode: dark, light, or night. dusk is design-system-only and rejected by the package loader."}},"repository_outputs":{"type":"object","required":["dark","light"],"properties":{"dark":{"type":"string"},"light":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"selector_only":{"const":true},"install":{"type":"object","required":["module","destinations","allowed_ownership"],"properties":{"module":{"type":"string"},"destinations":{"type":"array","items":{"type":"string"}},"allowed_ownership":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"activation":{"type":"object","required":["selector","validate","reload","rollback"],"properties":{"selector":{"type":"string"},"validate":{"type":"object"},"reload":{"type":"object","required":["kind","observable"],"properties":{"kind":{"type":"string"},"observable":{"type":"boolean"}},"additionalProperties":false},"rollback":{"type":"string"}},"additionalProperties":false},"version":{"type":"object","required":["constraint"],"properties":{"constraint":{"type":["string","null"]}},"additionalProperties":false},"excluded_behavior":{"type":"string","minLength":1}},"allOf":[{"anyOf":[{"required":["render"]},{"required":["selector_only"]}]},{"if":{"properties":{"classification":{"const":"excluded"}}},"then":{"required":["excluded_behavior"]}}],"additionalProperties":false}}} +{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","required":["$schema","schema","targets"],"properties":{"$schema":{"const":"./targets.schema.json"},"schema":{"const":"dreamcoder.theme-targets.v1"},"targets":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/target"}}},"additionalProperties":false,"$defs":{"target":{"type":"object","required":["id","classification","reason","owner","availability","install","activation","version"],"properties":{"id":{"type":"string","minLength":1},"classification":{"enum":["required","optional","excluded"]},"reason":{"type":"string","minLength":1},"owner":{"type":"string","minLength":1},"availability":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string"}},"additionalProperties":false},"render":{"type":"object","required":["renderer","modes","repository_outputs"],"properties":{"renderer":{"type":"string"},"modes":{"type":"array","minItems":1,"items":{"type":"string","description":"Runtime render mode: dark or light. dusk is design-system-only and rejected by the package loader."}},"repository_outputs":{"type":"object","required":["dark","light"],"properties":{"dark":{"type":"string"},"light":{"type":"string"}},"additionalProperties":false}},"additionalProperties":false},"selector_only":{"const":true},"install":{"type":"object","required":["module","destinations","allowed_ownership"],"properties":{"module":{"type":"string"},"destinations":{"type":"array","items":{"type":"string"}},"allowed_ownership":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"activation":{"type":"object","required":["selector","validate","reload","rollback"],"properties":{"selector":{"type":"string"},"validate":{"type":"object"},"reload":{"type":"object","required":["kind","observable"],"properties":{"kind":{"type":"string"},"observable":{"type":"boolean"}},"additionalProperties":false},"rollback":{"type":"string"}},"additionalProperties":false},"version":{"type":"object","required":["constraint"],"properties":{"constraint":{"type":["string","null"]}},"additionalProperties":false},"excluded_behavior":{"type":"string","minLength":1}},"allOf":[{"anyOf":[{"required":["render"]},{"required":["selector_only"]}]},{"if":{"properties":{"classification":{"const":"excluded"}}},"then":{"required":["excluded_behavior"]}}],"additionalProperties":false}}} diff --git a/DreamcoderThemes/dreamcoder/tmux-dreamcoder-night.conf b/DreamcoderThemes/dreamcoder/tmux-dreamcoder-night.conf deleted file mode 100644 index 0dc431d4..00000000 --- a/DreamcoderThemes/dreamcoder/tmux-dreamcoder-night.conf +++ /dev/null @@ -1,47 +0,0 @@ -# Dreamcoder Dark Night — tmux dark theme -# Text-only minimal hierarchy: no blocks, no pills, no powerline. -# Source: tmux source-file ~/.config/tmux/tmux-dreamcoder-dark.conf - -# Default terminal colours (truecolor) -set -g default-terminal "tmux-256color" -set -ga terminal-overrides ",*256col*:Tc" -set -ga terminal-overrides '*:Ss=\E[%p1%d q:Se=\E[2 q' - -# Panes -set -g pane-border-style "fg=#1b1b1b,bg=default" -set -g pane-active-border-style "fg=#3a76d6,bg=default" -set -g display-panes-colour "#a4afee" -set -g display-panes-active-colour "#a4afee" -# Semantic token for status integrations that report successful commands. -set -g @dreamcoder-success-colour "#43b48b" - -# ─── Minimal status bar (text-only hierarchy) ───────── -set -g status-style "bg=#090909" -set -g status-position top -set -g status-justify left -set -g status-interval 5 -set -g status-left-length 60 -set -g status-right-length 120 - -# Left: session name bold + middot separator -set -g status-left "#[fg=#c6c6c6,bold] #S #[fg=#ababab]· " - -# Window tabs — text-only, muted / accent active -set -g window-status-style "fg=#ababab" -setw -g window-status-format "#I:#W " -set -g window-status-current-style "fg=#a4afee,bold" -setw -g window-status-current-format "#I:#W " -setw -g window-status-separator "" - -# Right: path muted + time bold -set -g status-right "#[fg=#ababab]#{b:pane_current_path} #[fg=#c6c6c6,bold]%H:%M " - -# Message -set -g message-style "fg=#c6c6c6,bg=#090909" -set -g message-command-style "fg=#c6c6c6,bg=#090909" - -# Mode (copy mode) -setw -g mode-style "fg=#000000,bg=#3a76d6" - -# Bell -setw -g window-status-bell-style "fg=#000000,bg=#e88484,bold" diff --git a/DreamcoderThemes/dreamcoder/tokens.json b/DreamcoderThemes/dreamcoder/tokens.json index 3f18682d..4aed1acb 100644 --- a/DreamcoderThemes/dreamcoder/tokens.json +++ b/DreamcoderThemes/dreamcoder/tokens.json @@ -198,14 +198,6 @@ "scrim": "rgba(26, 18, 12, 0.42)" } }, - "render_profiles": { - "night": { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02 - } - }, "guardrails": { "minimum_text_contrast": 4.5, "preferred_main_text_contrast": 7.0, diff --git a/DreamcoderThemes/dreamcoder/tokens.schema.json b/DreamcoderThemes/dreamcoder/tokens.schema.json index 43f9a5e3..f6d69168 100644 --- a/DreamcoderThemes/dreamcoder/tokens.schema.json +++ b/DreamcoderThemes/dreamcoder/tokens.schema.json @@ -2,49 +2,12 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Dreamcoder OS design tokens", "type": "object", - "required": ["name", "version", "modes", "render_profiles", "guardrails"], + "required": ["name", "version", "modes", "guardrails"], "properties": { "$schema": { "type": "string" }, "name": { "const": "Dreamcoder OS" }, "version": { "type": "integer", "minimum": 1 }, "description": { "type": "string" }, - "render_profiles": { - "type": "object", - "required": ["night"], - "properties": { - "night": { - "type": "object", - "required": [ - "brightness_factor", - "saturation_factor", - "maximum_corrective_delta", - "corrective_step" - ], - "properties": { - "brightness_factor": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - "saturation_factor": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 1 - }, - "maximum_corrective_delta": { - "type": "number", - "minimum": 0, - "maximum": 0.2 - }, - "corrective_step": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 0.2 - } - } - } - } - }, "modes": { "type": "object", "required": ["dark", "light", "dusk"], diff --git a/DreamcoderThemes/dreamcoder/waybar-night.css b/DreamcoderThemes/dreamcoder/waybar-night.css deleted file mode 100644 index 072a737e..00000000 --- a/DreamcoderThemes/dreamcoder/waybar-night.css +++ /dev/null @@ -1,57 +0,0 @@ -/* Dreamcoder color layer for Waybar. */ -@define-color bg #000000; -@define-color bg-soft #090909; -@define-color surface #090909; -@define-color surface-2 #1b1b1b; -@define-color surface-3 #282828; -@define-color text #c6c6c6; -@define-color text-heading #d3d3d3; -@define-color muted #ababab; -@define-color subtle #959595; -@define-color comment #b3b3b3; -@define-color border #5c5c5c; -@define-color border-ui #797979; -@define-color focus #3a76d6; -@define-color accent #a4afee; -@define-color accent-2 #b48ce9; -@define-color diagnostic #62b9e2; -@define-color lavender #b48ce9; -@define-color error #e88484; -@define-color success #43b48b; -@define-color warning #d1a126; -@define-color link #a4afee; -@define-color link-hover #b48ce9; - -window#waybar { - background: rgba(11, 11, 13, 0.92); - color: @text; - border-bottom: 1px solid alpha(@accent, 0.26); -} - -#workspaces button { - color: @muted; - background: transparent; - border: 1px solid transparent; -} - -#workspaces button.active { - color: @accent; - background: rgba(81, 84, 212, 0.28); - border-color: alpha(@focus, 0.82); -} - -#clock, -#battery, -#network, -#pulseaudio, -#cpu, -#memory, -#tray { - background: rgba(198, 198, 198, 0.08); - color: @text; - border: 1px solid alpha(@border-ui, 0.78); -} - -#battery.warning { color: @warning; } -#battery.critical { color: @error; } -#network.disconnected { color: @error; } diff --git a/DreamcoderThemes/dreamcoder/zsh-syntax-highlighting-dreamcoder-night.zsh b/DreamcoderThemes/dreamcoder/zsh-syntax-highlighting-dreamcoder-night.zsh deleted file mode 100644 index c318c4d0..00000000 --- a/DreamcoderThemes/dreamcoder/zsh-syntax-highlighting-dreamcoder-night.zsh +++ /dev/null @@ -1,12 +0,0 @@ -# Dreamcoder Dark Night — zsh-syntax-highlighting -typeset -A ZSH_HIGHLIGHT_STYLES -ZSH_HIGHLIGHT_STYLES[default]='fg=#c6c6c6'; ZSH_HIGHLIGHT_STYLES[unknown-token]='fg=#e88484'; ZSH_HIGHLIGHT_STYLES[reserved-word]='fg=#a4afee,bold'; ZSH_HIGHLIGHT_STYLES[alias]='fg=#b48ce9' -ZSH_HIGHLIGHT_STYLES[suffix-alias]='fg=#b48ce9'; ZSH_HIGHLIGHT_STYLES[builtin]='fg=#a4afee'; ZSH_HIGHLIGHT_STYLES[function]='fg=#b48ce9'; ZSH_HIGHLIGHT_STYLES[command]='fg=#a4afee' -ZSH_HIGHLIGHT_STYLES[precommand]='fg=#a4afee,italic'; ZSH_HIGHLIGHT_STYLES[commandseparator]='fg=#ababab'; ZSH_HIGHLIGHT_STYLES[hashed-command]='fg=#a4afee'; ZSH_HIGHLIGHT_STYLES[path]='fg=#62b9e2' -ZSH_HIGHLIGHT_STYLES[path_pathseparator]='fg=#a4afee'; ZSH_HIGHLIGHT_STYLES[path_prefix]='fg=#62b9e2,underline'; ZSH_HIGHLIGHT_STYLES[path_approx]='fg=#d1a126,underline'; ZSH_HIGHLIGHT_STYLES[globbing]='fg=#b48ce9' -ZSH_HIGHLIGHT_STYLES[history-expansion]='fg=#b48ce9'; ZSH_HIGHLIGHT_STYLES[single-hyphen-option]='fg=#62b9e2'; ZSH_HIGHLIGHT_STYLES[double-hyphen-option]='fg=#62b9e2'; ZSH_HIGHLIGHT_STYLES[back-quoted-argument]='fg=#43b48b' -ZSH_HIGHLIGHT_STYLES[single-quoted-argument]='fg=#43b48b'; ZSH_HIGHLIGHT_STYLES[double-quoted-argument]='fg=#43b48b'; ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument]='fg=#43b48b'; ZSH_HIGHLIGHT_STYLES[rc-quote]='fg=#b98beb' -ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument]='fg=#b98beb'; ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument]='fg=#b98beb'; ZSH_HIGHLIGHT_STYLES[back-dollar-quoted-argument]='fg=#b98beb'; ZSH_HIGHLIGHT_STYLES[assign]='fg=#c6c6c6' -ZSH_HIGHLIGHT_STYLES[redirection]='fg=#b48ce9'; ZSH_HIGHLIGHT_STYLES[comment]='fg=#b3b3b3,italic'; ZSH_HIGHLIGHT_STYLES[variable]='fg=#b98beb'; ZSH_HIGHLIGHT_STYLES[mathvar]='fg=#b98beb' -ZSH_HIGHLIGHT_STYLES[null]='fg=#ababab'; ZSH_HIGHLIGHT_STYLES[bracket-level-1]='fg=#a4afee'; ZSH_HIGHLIGHT_STYLES[bracket-level-2]='fg=#62b9e2'; ZSH_HIGHLIGHT_STYLES[bracket-level-3]='fg=#43b48b' -ZSH_HIGHLIGHT_STYLES[bracket-level-4]='fg=#b48ce9'; ZSH_HIGHLIGHT_STYLES[cursor-matchingbracket]='fg=#000000,bg=#a4afee' diff --git a/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Night.yaml b/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Night.yaml deleted file mode 100644 index 7a01c62e..00000000 --- a/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Night.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Dreamcoder Dark Night -accent: '#a4afee' -cursor: '#a4afee' -background: '#000000' -foreground: '#c6c6c6' -details: darker -terminal_colors: - normal: - black: '#777777' - red: '#e88484' - green: '#43b48b' - yellow: '#d1a126' - blue: '#62b9e2' - magenta: '#b98beb' - cyan: '#b48ce9' - white: '#ababab' - bright: - black: '#959595' - red: '#e29090' - green: '#5bb796' - yellow: '#cfa843' - blue: '#74bbdd' - magenta: '#bb96e4' - cyan: '#5384d3' - white: '#c6c6c6' diff --git a/DreamcoderZellij/.config/zellij/dreamcoder-night.kdl b/DreamcoderZellij/.config/zellij/dreamcoder-night.kdl deleted file mode 100644 index 4f61a6a6..00000000 --- a/DreamcoderZellij/.config/zellij/dreamcoder-night.kdl +++ /dev/null @@ -1,39 +0,0 @@ -// Dreamcoder Dark Night — Zellij theme (dreamcoder-night) -// Auto-generated from tokens.json — do not edit manually - -themes { - dreamcoder-night { - bg "#000000" - fg "#c6c6c6" - black "#000000" - red "#e88484" - green "#43b48b" - yellow "#d1a126" - blue "#62b9e2" - magenta "#b98beb" - cyan "#62b9e2" - white "#c6c6c6" - orange "#d1a126" - gray "#ababab" - - // UI elements - border "#5c5c5c" - border_active "#a4afee" - border_unfocused "#5c5c5c" - - // Tab bar - tab_bar_bg "#0b0b0d" - tab_active_bg "#5c5c5c" - tab_active_fg "#a4afee" - tab_inactive_bg "#0b0b0d" - tab_inactive_fg "#ababab" - - // Pane frames - pane_frame_bg "#0b0b0d" - pane_frame_fg "#5c5c5c" - - // Selection - selection_bg "#a4afee" - selection_fg "#000000" - } -} diff --git a/INSTALL.md b/INSTALL.md index ad36a9d6..8ba2fc96 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -142,8 +142,7 @@ systemctl --user status dreamcoder-theme-auto.timer The timer switches modes by schedule: -- **07:00-16:00** → Light -- **16:00-18:00** → Night +- **07:00-18:00** → Light - **18:00-07:00** → Dark --- diff --git a/docs/DREAMCODER_DESIGN_SYSTEM.md b/docs/DREAMCODER_DESIGN_SYSTEM.md index 7b19490c..d6624faf 100644 --- a/docs/DREAMCODER_DESIGN_SYSTEM.md +++ b/docs/DREAMCODER_DESIGN_SYSTEM.md @@ -21,7 +21,7 @@ The system competes as a **developer OS design system**, not as a web component Required contract: - **Versioned token schema**: `DreamcoderThemes/dreamcoder/tokens.schema.json` defines the public token shape. -- **Mode parity**: `light`, `dusk`, and `dark` must expose equivalent semantic roles; the derived `night` render profile preserves those roles from Dreamcoder Dark. +- **Mode parity**: `light`, `dusk`, and `dark` must expose equivalent semantic roles. - **Semantic separation**: `accent`, `accent_2`, `diagnostic`, `warning`, `error`, `comment`, `subtle`, `focus`, `border_ui`, and `border_hi` must remain distinct. - **Generator ownership**: app-specific files are outputs; renderer modules own translation from tokens to target syntax. - **No silent drift**: regenerated artifacts must be checked into the repo or intentionally ignored. @@ -62,16 +62,14 @@ Minimums: - On-accent text: Lc 60 - Heading text: Lc 60 light / 45 dark -See `DreamcoderThemes/dreamcoder/tokens.json` guardrails for current values. `scripts/verify-theme-health.py` validates Light, Dark, and the derived Night candidate, and any below-floor pair blocks the command. +See `DreamcoderThemes/dreamcoder/tokens.json` guardrails for current values. `scripts/verify-theme-health.py` validates Light, Dark, and Dusk, and any below-floor pair blocks the command. ## Health verification policy -`python scripts/verify-theme-health.py` is the blocking health command for Dreamcoder Workbench theme changes. It validates the in-memory design-system contract matrix for `dark`, `light`, and `night`; any contract finding fails health verification. +`python scripts/verify-theme-health.py` is the blocking health command for Dreamcoder Workbench theme changes. It validates the in-memory design-system contract matrix for `dark`, `light`, and `dusk`; any contract finding fails health verification. OpenCode theme ownership is limited to `.opencode/themes/`. The health check requires exactly `dreamcoder.json` there. Application configuration under `DreamcoderOpenCode/.config/opencode/`, including `opencode.json`, is not a theme artifact and is intentionally excluded. -`night` is the low-light render profile derived from Dreamcoder Dark (reduced brightness and saturation via the canonical `render_profiles`). The health check validates it, and runtime activation is supported through `dreamcoder night`. - ## Governance Dreamcoder Workbench changes follow this governance model: @@ -83,7 +81,7 @@ Dreamcoder Workbench changes follow this governance model: 5. **Release notes**: user-visible theme, CLI, repair, or governance changes need a changelog entry. 6. **Compatibility check**: repair/install flows must remain safe after ML4W, Gentleman, Waypaper, or Hyprland updates. -⚠️ **Known token gaps:** the corrected dual gate surfaces pre-existing debt that Phase 2 corrected in tokens (dark `subtle` → Lc 44.0, `border_ui` WCAG ≥ 4.5, light `disabled` WCAG ≥ 4.5, light/night `success` APCA ≥ 75). The gate is now passing on all canonical palettes; any future below-floor pair blocks health verification. +⚠️ **Known token gaps:** the corrected dual gate surfaces pre-existing debt that Phase 2 corrected in tokens (dark `subtle` → Lc 44.0, `border_ui` WCAG ≥ 4.5, light `disabled` WCAG ≥ 4.5, light `success` APCA ≥ 75). The gate is now passing on all canonical palettes; any future below-floor pair blocks health verification. ## Release readiness checklist diff --git a/docs/configuration/editor-config.md b/docs/configuration/editor-config.md index 31d62621..23140519 100644 --- a/docs/configuration/editor-config.md +++ b/docs/configuration/editor-config.md @@ -22,7 +22,7 @@ Dreamcoder colorscheme generated from `tokens.json`: return { "dreamcoder.nvim", opts = { - variant = "dark", -- or "light" or "night" + variant = "dark", -- or "light" }, } ``` diff --git a/docs/configuration/ml4w.md b/docs/configuration/ml4w.md index e17f97d5..e5d30f36 100644 --- a/docs/configuration/ml4w.md +++ b/docs/configuration/ml4w.md @@ -149,9 +149,7 @@ one of those files silently disappears on the next upgrade. The rule: restarted with `~/.config/ml4w/listeners.sh --restart gtk-theme-switcher` (it keeps the body it parsed at start). A listener without a Matugen call is left untouched with a warning; a missing listener is skipped. Override the - paths with `ML4W_GTK_LISTENER` and `ML4W_LISTENERS_SCRIPT`. Known gap: with - the Night render profile persisted, `sync` only regenerates repository - artifacts, so the block does not restore live Night colours. + paths with `ML4W_GTK_LISTENER` and `ML4W_LISTENERS_SCRIPT`. - **waypaper is optional.** ML4W 2.16 no longer installs it; its `post_command` is hooked only when `~/.config/waypaper/config.ini` exists. - **Colour files may be regular files.** ML4W 2.16 ships diff --git a/docs/configuration/terminal-config.md b/docs/configuration/terminal-config.md index c2c29608..2d399db5 100644 --- a/docs/configuration/terminal-config.md +++ b/docs/configuration/terminal-config.md @@ -14,7 +14,7 @@ Terminals auto-detect `DREAMCODER_THEME_MODE` env var: ```bash -export DREAMCODER_THEME_MODE="dark" # or "light" or "night" +export DREAMCODER_THEME_MODE="dark" # or "light" ``` ## Font Configuration diff --git a/docs/configuration/theme-system.md b/docs/configuration/theme-system.md index a2654551..30d862ce 100644 --- a/docs/configuration/theme-system.md +++ b/docs/configuration/theme-system.md @@ -20,8 +20,7 @@ tokens.json → generate-palette-tokens.py → palette_tokens.py Single source of truth: [`DreamcoderThemes/dreamcoder/tokens.json`](../../DreamcoderThemes/dreamcoder/tokens.json) The canonical modes are exactly **Dreamcoder Dark**, **Dreamcoder Light**, and -**Dreamcoder Dusk**. Night is a render profile derived from Dark, not a fourth -mode. Dark's `surface_policy` permits pure black only for the canvas; functional +**Dreamcoder Dusk**; only Dark and Light are user-facing. Dark's `surface_policy` permits pure black only for the canvas; functional and scrollable surfaces use the near-black ladder (`surface0`–`surface3`) to reduce OLED smear. diff --git a/docs/generated/DREAMCODER_OPERATOR_REPORT.md b/docs/generated/DREAMCODER_OPERATOR_REPORT.md index de3c4c16..d015b8a5 100644 --- a/docs/generated/DREAMCODER_OPERATOR_REPORT.md +++ b/docs/generated/DREAMCODER_OPERATOR_REPORT.md @@ -25,8 +25,6 @@ Generated from current machine state and Control Center contracts. │ Active machine profile name. │ │ motion.active = balanced │ │ Active motion preset. (battery, balanced,│ -│ theme.render_profile = standard │ -│ Theme rendering profile (standard or nigh│ ├─────────────────────────────────────────────┤ │ Apply: dreamcoder tui set │ │ Safe: add --dry-run --json before applying │ diff --git a/docs/generated/dreamcoder-theme-preview.md b/docs/generated/dreamcoder-theme-preview.md index dfd4808d..19d97d0c 100644 --- a/docs/generated/dreamcoder-theme-preview.md +++ b/docs/generated/dreamcoder-theme-preview.md @@ -240,44 +240,6 @@ Semantic tokens are intentionally distinct: | `border_hi` | 8.10:1 | PASS | | `focus` | 4.92:1 | PASS | -### Night (render profile derived from Dreamcoder Dark) contrast (WCAG 2) - -| Token | Ratio vs bg | Target | -| --- | ---: | --- | -| `text` | 12.29:1 | AAA | -| `muted` | 9.14:1 | AA | -| `comment` | 10.02:1 | AA | -| `accent` | 9.95:1 | AA | -| `accent_2` | 7.87:1 | AA | -| `diagnostic` | 9.56:1 | AA | -| `sage` | 8.14:1 | AA | -| `error` | 8.06:1 | AA | -| `warning` | 8.84:1 | AA | - -### Night (render profile derived from Dreamcoder Dark) APCA - -| Token | Lc vs bg | Target | -| --- | ---: | --- | -| `text` | 72.1 | ≥75 (FAIL) | -| `muted` | 56.8 | ≥75 (FAIL) | -| `comment` | 61.2 | ≥75 (FAIL) | -| `accent` | 60.9 | ≥75 (FAIL) | -| `accent_2` | 50.1 | ≥75 (FAIL) | -| `diagnostic` | 59.1 | ≥75 (FAIL) | -| `sage` | 51.8 | ≥75 (FAIL) | -| `error` | 51.4 | ≥75 (FAIL) | -| `warning` | 55.4 | ≥75 (FAIL) | -| `border_ui` | 31.5 | ≥60 (FAIL) | -| `focus` | 31.3 | ≥60 (FAIL) | - -### Night (render profile derived from Dreamcoder Dark) UI affordance contrast - -| Token | Ratio vs bg | Target | -| --- | ---: | --- | -| `border_ui` | 4.82:1 | PASS | -| `border_hi` | 7.01:1 | PASS | -| `focus` | 4.74:1 | PASS | - ## Usage ```bash diff --git a/docs/herdr.md b/docs/herdr.md index 92ca36fd..cf3f431a 100644 --- a/docs/herdr.md +++ b/docs/herdr.md @@ -87,16 +87,12 @@ drift is detectable: DreamcoderHerdr/.config/herdr/dreamcoder/ 0.7.3/config.dark.toml 0.7.3/config.light.toml - 0.7.3/config.night.toml 0.8.0/config.dark.toml 0.8.0/config.light.toml - 0.8.0/config.night.toml 0.8.2/config.dark.toml 0.8.2/config.light.toml - 0.8.2/config.night.toml 0.9.1/config.dark.toml 0.9.1/config.light.toml - 0.9.1/config.night.toml ``` - Each variant carries the header `# Managed by Dreamcoder; repository variant only.` @@ -114,7 +110,7 @@ DreamcoderHerdr/.config/herdr/dreamcoder/ `DreamcoderProfiles/deploy/` holds repository-safe deployment profiles: - `desktop-arch.json` — desktop Arch Linux deployment; supports the full - day/night cycle via systemd timers. Herdr `[ui] pane_scrollbars` follows the + light/dark schedule via systemd timers. Herdr `[ui] pane_scrollbars` follows the Herdr default (`false`). - `mobile-termux.json` — mobile Termux/Moshi deployment; selects Dreamcoder Light and disables Herdr pane scrollbars for narrow screens. It contains @@ -140,7 +136,7 @@ safely. ## Live switching `scripts/herdr-theme-switch.sh` detects the installed Herdr version, selects -the matching generated dark, light, or night variant, and requests a live config +the matching generated dark or light variant, and requests a live config reload. It resolves the selector from `HERDR_CONFIG_PATH` first, then `XDG_CONFIG_HOME`, then `~/.config/herdr/config.toml`. diff --git a/docs/installation/linux.md b/docs/installation/linux.md index 43181723..0dfaa726 100644 --- a/docs/installation/linux.md +++ b/docs/installation/linux.md @@ -98,7 +98,7 @@ dreamcoder light # → Cocoa/Lúcuma dreamcoder status # → System overview # Automatic (via systemd timer) -# Switches at 07:00 (light), 16:00 (night), 18:00 (dark) +# Switches at 07:00 (light) and 18:00 (dark) systemctl --user status dreamcoder-theme-auto.timer ``` diff --git a/docs/installation/termux.md b/docs/installation/termux.md index 2cf0217a..74cfbded 100644 --- a/docs/installation/termux.md +++ b/docs/installation/termux.md @@ -38,7 +38,7 @@ dreamcoder-theme sync ## Limitations -- No systemd (day/night automation unavailable) +- No systemd (light/dark automation unavailable) - No GUI apps (Kitty/Ghostty unavailable) - Limited shader support diff --git a/docs/migration/from-gentleman.md b/docs/migration/from-gentleman.md index 7b3b7a02..944bd5e5 100644 --- a/docs/migration/from-gentleman.md +++ b/docs/migration/from-gentleman.md @@ -67,5 +67,5 @@ The installer writes a backup manifest under `~/.local/share/dreamcoder/backups/ | Theme | Hardcoded | Token-based | | AI layer | gentle-ai | gentle-ai + agent themes (see [AI Integration](../ai-integration.md)) | | Vim trainer | ✓ | ✓ (improved) | -| Day/Night | — | ✓ Systemd timer | +| Light/Dark schedule | — | ✓ Systemd timer | | Health checks | — | ✓ WCAG/APCA | diff --git a/docs/migration/from-ml4w.md b/docs/migration/from-ml4w.md index 56e2fbb4..b4597500 100644 --- a/docs/migration/from-ml4w.md +++ b/docs/migration/from-ml4w.md @@ -59,5 +59,5 @@ The installer writes a backup manifest under `~/.local/share/dreamcoder/backups/ | Feature | ML4W | Dreamcoder Workbench | | --- | --- | --- | | Theme source | Wallpaper (Material You) | tokens.json | -| Day/Night | Manual | Systemd timer | +| Light/Dark schedule | Manual | Systemd timer | | Health checks | None | WCAG/APCA | diff --git a/odd/tasks/remove-night-profile.md b/odd/tasks/remove-night-profile.md new file mode 100644 index 00000000..974b1b90 --- /dev/null +++ b/odd/tasks/remove-night-profile.md @@ -0,0 +1,113 @@ +# Remove the Night render profile + +## Objective + +The Dreamcoder design system has exactly two modes: Dreamcoder Light and +Dreamcoder Dark. Remove the Night render profile (derived from Dark) end to end. + +## Problem + +Night survives as an opt-in profile (`dreamcoder night`, `theme.render_profile`, +`night_palette()`, 33 generated `*-night*` artifacts, Herdr night variants, docs). +The scheduler never activates it, and the 16:00 timer trigger only re-applies Light. + +## Why + +User decision (2026-09-28): "solo debe tener dreamcoder light y dreamcoder dark". + +## Scope + +- In: runtime code (palette, settings, settings_store, sync, cli_handlers, + cli_parser, herdr_switch, renderer_contract, targets, renderers, writers, doctor, + repair_engine), scripts (apply-theme-mode.sh, dreamcoder dispatcher, theme-auto.sh, + verify-theme-health.py, generate-theme-preview.py), data files (tokens.json, + tokens.schema.json, design-system.json, targets.json, targets.schema.json), + generated `*-night*` artifacts, tests, live docs, systemd timer (drop 16:00), + live-machine cleanup (settings key, dangling night symlinks). +- Out: the legacy Dusk token set (still used; separate decision); archived openspec, + CHANGELOG history and docs/superpowers history are not rewritten. + +## Constraints + +- Light and Dark output must stay byte-identical except where Night logic lived. +- Keep the Light/Dark contrast gate (`validate_palette`) intact. +- `targets.json`, `targets.schema.json` and `renderer_contract` change in the same + commit (`targets.py` requires modes to match `ALL_RENDER_VARIANTS`). +- Unrelated working-tree edits (timer-rewritten theme files, user's fish prompt, + warp, apply-system-mode.sh) are never staged. +- TDD: off (no project/session TDD configuration). Runners: `python -m pytest tests/`, + `bats tests/shell/ tests/ml4w/`. +- Delivery: work-unit commits on `refactor/remove-night-profile` (stacked on + `chore/ml4w-2.16-gentleman-sync`); forecast ~1,200–1,500 authored lines, mostly + deletions. Push/PR slicing is the user's decision. + +## Tasks + +- [x] N1 — Core model: remove Night from palette/settings/settings_store, renderer + contract, targets manifest + schema, tokens + schema, design-system mode_policy, + every renderer's `modes` and Night name-sniffing; update affected tests. Route: + delegated writer. Scope widened to keep the commit green: `sync.py` and the CLI + activation (`cli_handlers`, `cli_parser`, `herdr_switch`) plus the Python scripts + that imported `night_palette` (`verify-theme-health.py`, `generate-theme-preview.py`) + moved here from N2 because they import the removed palette/settings API. COVERAGE + stays as the 33-consumer declaration keyed by the Dark artifact; Codex app and + Antigravity coverage now render exactly what `sync_repo_snippets()` writes (active + palette / Dark-pinned). A persisted legacy `theme.render_profile` is ignored as an + unknown setting (warning, preserved). Repo-only regeneration: every Light/Dark + artifact byte-identical; only the generated README, nvim dispatcher and preview + docs lost their Night lines. Commit: 319ffdd. +- [x] N2 — Scripts + shell tests: `apply-theme-mode.sh` drops the profile argument, + the Night artifact gate, the kanagawa Night case and `DREAMCODER_THEME_PROFILE` + (cursor-cli.env no longer writes it; tmux unsets the stale variable); the + `dreamcoder` dispatcher rejects `night` as an unknown command; `theme-auto.sh` + stops passing `standard`; `pi-theme.sh` selects by mode only. Bats: Night tests + (including the failing kanagawa one) deleted and replaced with Light/Dark and + `night`-rejection coverage. Route: delegated writer. Python parts of the original + N2 landed in N1 (see above). Commit: 72c173b. +- [x] N3 — Artifacts: deleted the 33 tracked `*-night*` generated artifacts (Herdr + `config.night.toml` for 0.7.3/0.8.0/0.8.2/0.9.1 included) and the CI workflow's + Kitty night symlink. Repo-only regeneration afterwards reports 0 changes, so sync + no longer recreates them. Light/Dark byte-identity vs the pre-N1 baseline: every + tracked `*dark*`/`*light*` artifact unchanged. The characterization fixtures + (`tests/fixtures/legacy_*.json`) were intentionally NOT regenerated: they are frozen + Phase-0 evidence of the legacy 32-consumer sync (no test reads them), a rerun would + replace the legacy hashes with today's palette and bake machine-specific live + paths; the harness itself was updated in N1 so it still runs. Route: delegated + writer. Commit: e9b75fd. +- [x] N4 — Docs + timer: live docs (CLAUDE.md, README, INSTALL, AGENTS, CONTRIBUTING, + COMPARISON, design system, theme-system/ml4w/editor/terminal/termux, migration, + herdr + Herdr evidence, palette-token skill), live openspec specs (eye-comfort + rewritten around Light/Dark + legacy-key tolerance, ml4w-keybindings-waybar, + renderer-registry), `desktop-arch.json` wording, repo `settings.json` drops + `theme.render_profile`, and the timer drops the 16:00 trigger ("light/dark" + wording; schedule is 07:00 light, 18:00 dark). Not touched: archived and + in-flight `openspec/changes/*`, CHANGELOG, docs/superpowers, the Dusk + `light_hours_default`/`dusk_hours_default` token metadata. Route: delegated + writer. Commit: ad41d7b. +- [x] N5 — Live cleanup (2026-09-29): live `settings.json` already had no + `theme.render_profile`; the timer is a repo symlink and already fires only 07:00 and + 18:00; removed the two dangling links `~/.config/starship-night.toml` and Warp + `Dreamcoder-Night.yaml`; `systemctl --user daemon-reload`; `dreamcoder sync`; + real `dreamcoder dark` → `dreamcoder light` toggle leaves Starship, Hyprland and + Waybar in Light; `verify-ml4w-setup.sh` 20 passed / 0 failed; `dreamcoder doctor` + guardrails passed; `dreamcoder night` is rejected as an unknown command. Route: inline. + +## Acceptance criteria + +- `git ls-files | grep -i night` returns nothing outside history docs. +- `dreamcoder night` is an unknown command; `dreamcoder light|dark` work. +- pytest and bats green (the kanagawa Night bats test is removed with Night). +- `verify-ml4w-setup.sh` and `dreamcoder doctor` pass after N5. + +## Progress + +- Verification after N4: `python -m pytest tests/ -q` exit 0; `bats tests/shell/ + tests/ml4w/` 143/143 ok; `verify-theme-health.py` and `verify-repo-sync.py` OK; + shellcheck/bash -n clean on modified scripts; `git ls-files | grep -i night` + lists only this feature document. +- Branch created from `chore/ml4w-2.16-gentleman-sync`; shell readability fixes + (c9cf723, 32f9f6f, ffc5359, 754d352) landed here before the Night work started. + +## Next step + +Feature complete. Pending user decisions: push / PR slicing of the two stacked branches. diff --git a/openspec/specs/eye-comfort/spec.md b/openspec/specs/eye-comfort/spec.md index f129c013..d88a9591 100644 --- a/openspec/specs/eye-comfort/spec.md +++ b/openspec/specs/eye-comfort/spec.md @@ -2,7 +2,7 @@ ## Purpose -Define the Night/Dim eye-comfort rendering profile for Dreamcoder OS: one canonical APCA contrast implementation, independent blocking WCAG 2.2 + APCA gates, a deterministic brightness/saturation-reduced derivation of the dark Anthracite Steel palette, persistent `theme.render_profile` settings with `dreamcoder night` CLI activation, all-32-active-target coverage, and fail-closed validation before any write — without medical claims, automatic scheduling, palette redesign, or renderer-interface changes. +Define the eye-comfort contract for Dreamcoder OS: one canonical APCA contrast implementation, independent blocking WCAG 2.2 + APCA gates, exactly two user-facing modes (Dreamcoder Light and Dreamcoder Dark) activated through the Dreamcoder CLI, declared coverage of every active color consumer, and fail-closed validation before any write — without medical claims, palette redesign, or renderer-interface changes. ## Requirements @@ -52,141 +52,73 @@ All enforced text and affordance pairs MUST satisfy BOTH the WCAG 2.2 minimum co #### Scenario: Class floors apply per content and mode -- GIVEN heading (Lc 60 light / Lc 45 dark), quiet (Lc 44), UI (Lc 60 light / Lc 28 dark), and on-accent (Lc 60) pairs across Light, Dark, and Night +- GIVEN heading (Lc 60 light / Lc 45 dark), quiet (Lc 44), UI (Lc 60 light / Lc 28 dark), and on-accent (Lc 60) pairs across Light, Dark, and Dusk - WHEN health validation runs - THEN each pair MUST be measured against its declared class and mode floor, and any below-floor pair MUST block -### Requirement: Deterministic Night/Dim palette transformation - -The system MUST derive the Night/Dim palette in `src/dreamcoder_theme/palette.py` by applying a brightness- and saturation-reduced transform to the dark Anthracite Steel palette, after `adaptive_palette()` and before any renderer runs. The transform MUST use explicit, bounded profile parameters represented in the canonical token contract or its schema, and MUST NOT hand-tune colors inside individual renderers. The transform MUST preserve token keys, semantic relationships, pure-black/white avoidance, alpha syntax, and the `dict[str, str]` renderer input shape. For identical canonical tokens, wallpaper/adaptive input, and profile settings, the transform MUST produce byte-identical output. - -#### Scenario: Night derives from dark Anthracite Steel - -- GIVEN the standard dark Anthracite Steel palette -- WHEN the Night/Dim transform is applied after `adaptive_palette()` and before any renderer runs -- THEN every output token is a brightness/saturation-reduced derivation of the corresponding dark token with the same semantic role, and no individual renderer adjusts colors - -#### Scenario: Identical inputs produce identical output - -- GIVEN identical canonical tokens, adaptive input, and profile settings -- WHEN the Night transform runs twice -- THEN both runs MUST produce byte-identical palettes - -#### Scenario: Keys, alpha syntax, and input shape are preserved - -- GIVEN a transformed Night palette -- WHEN it is passed to renderers -- THEN it contains exactly the same token keys and alpha syntax as the standard palette and consumes the same `dict[str, str]` input shape - ### Requirement: Pre-write validation and fail-closed write behavior -The final palette — including the transformed Night palette — MUST be validated by `validate_palette()` before the first writer runs, and validation MUST finish before `sync_active_targets()` and `sync_repo_snippets()` perform any write. If dimming causes any WCAG or APCA pair to miss its floor, generation MUST stop before writes, the command MUST exit non-zero, and no partial cross-target profile MAY be left applied. The transform MAY make a narrowly bounded corrective adjustment to restore a floor, but MUST NOT weaken a threshold and MUST NOT silently fall back to the standard dark palette. `write_if_changed()` semantics MUST be preserved. +The final Light or Dark palette MUST be validated by `validate_palette()` before the first writer runs, and validation MUST finish before `sync_active_targets()` and `sync_repo_snippets()` perform any write. If any WCAG or APCA pair misses its floor, generation MUST stop before writes, the command MUST exit non-zero, and no partial cross-target state MAY be left applied. `write_if_changed()` semantics MUST be preserved. #### Scenario: A failed gate blocks all writes -- GIVEN a Night palette whose transformed output misses one APCA floor -- WHEN the validated sync runs -- THEN no target or active output is written or selected, the command exits non-zero, and the prior standard profile remains active - -#### Scenario: Bounded corrective adjustment restores a floor - -- GIVEN dimming pushes one pair below its declared floor -- WHEN the transform applies a narrowly bounded corrective adjustment -- THEN the adjustment restores the floor without weakening any threshold, and the final palette passes validation before any write - -#### Scenario: No silent standard-dark fallback - -- GIVEN the transform cannot meet a floor after bounded correction -- WHEN the Night sync runs -- THEN it MUST fail closed with a diagnostic and MUST NOT emit standard dark output while reporting Night - -### Requirement: All-active-target Night coverage - -Night MUST apply to every consumer output in the union of `sync_active_targets()` and `sync_repo_snippets()` — the 32 active color consumers — and MUST NOT silently substitute standard dark for any of them. Each active consumer MUST have a deterministic Night artifact or a Night-selected active output where the target format supports named variants. Silent omission, standard-dark substitution, and partial success MUST be forbidden: no target MAY receive standard dark while the command reports Night. `targets.json` and its schema MUST be updated only as required to represent Night render coverage for the existing active color targets; `dusk-runtime` MUST remain excluded, and no new application targets beyond the 32 active consumers MAY be added. - -#### Scenario: All 32 active targets receive Night output - -- GIVEN the Night profile is active +- GIVEN a palette that misses one APCA floor - WHEN the validated sync runs -- THEN every one of the 32 consumers in the union of `sync_active_targets()` and `sync_repo_snippets()` receives Night output or a Night-selected active variant, and the coverage report lists each target - -#### Scenario: Standard-dark substitution is forbidden - -- GIVEN a consumer whose format supports named variants -- WHEN Night sync runs -- THEN that consumer MUST NOT be written with or left selecting standard dark while the command reports Night, and any such substitution MUST fail the command non-zero - -#### Scenario: Partial success fails closed - -- GIVEN Night generation succeeds for 31 consumers but fails for one -- WHEN the sync completes -- THEN the command exits non-zero and identifies the failed target, and MUST NOT report a silently partial profile as success +- THEN no target or active output is written or selected, the command exits non-zero, and the prior mode remains active -### Requirement: Persistent render profile setting +### Requirement: Declared coverage of every active color consumer -The system MUST add a typed setting `theme.render_profile` to `SETTINGS_SCHEMA` in `src/dreamcoder_theme/settings_store.py` with the closed values `standard` and `night`, and MUST preserve unknown settings for forward compatibility. A profile resolver in `src/dreamcoder_theme/settings.py` MUST read the persisted setting for sync, with an explicit environment override for isolated generation and tests. `theme_mode()` MUST remain responsible for the Light/Dark base and MUST NOT reinterpret Night as Dusk. +Every consumer output in the union of `sync_active_targets()` and `sync_repo_snippets()` MUST be declared in the sync coverage table and rendered in memory during preparation; an undeclared or unrendered consumer MUST fail the command before any write. `targets.json` render modes MUST be exactly `dark` and `light`; `dusk-runtime` MUST remain excluded. -#### Scenario: Persisted profile is resolved +#### Scenario: An undeclared consumer blocks preparation -- GIVEN `theme.render_profile=night` is persisted -- WHEN sync resolves the rendering profile -- THEN the resolver returns `night` absent an environment override, and sync uses the Night transform with the dark base +- GIVEN a consumer written by sync but missing from the coverage declaration +- WHEN preparation runs +- THEN the command fails closed before any write and names the consumer -#### Scenario: Environment override wins without mutation +### Requirement: Dreamcoder CLI Light/Dark activation -- GIVEN `theme.render_profile=night` is persisted -- WHEN an explicit environment override for the profile is set for an invocation -- THEN the resolver MUST return the override value for that invocation and MUST NOT mutate the persisted setting +`dreamcoder light` and `dreamcoder dark` MUST persist `terminal.default_mode`, run the validated sync, and return non-zero without changing active outputs when validation fails. `night` MUST be rejected as an unknown command, and `dreamcoder theme apply` MUST accept only `light` and `dark`. A settings file that still carries the retired `theme.render_profile` key MUST load without error: the key is reported as an unknown setting, preserved, and never read. `scripts/theme-auto.sh` MUST keep the Light/Dark schedule. -#### Scenario: Invalid profile value is rejected +#### Scenario: Light or Dark activation succeeds -- GIVEN `theme.render_profile` is set to a value other than `standard` or `night` -- WHEN settings validation runs -- THEN the value MUST be rejected or defaulted to `standard`, and MUST NOT be interpreted as a runtime profile - -### Requirement: Dreamcoder CLI activation and profile exit - -`dreamcoder night` MUST persist the Night profile, select the dark Anthracite Steel base, run the validated sync, and return non-zero without changing active outputs when validation fails. `dreamcoder light` and `dreamcoder dark` MUST remain backward compatible and MUST each explicitly persist `theme.render_profile=standard` before applying their base mode, exiting Night and regenerating the standard identity across the same target inventory. `dreamcoder settings get/set theme.render_profile` MUST remain available through the existing generic settings interface. The system MUST NOT add automatic time-based activation; `scripts/theme-auto.sh` MUST keep its current Light/Dark schedule. - -#### Scenario: Night activation succeeds - -- GIVEN the standard profile is active -- WHEN the user runs `dreamcoder night` -- THEN the Night profile is persisted, the dark base is selected, the Night transform is validated, and all 32 active targets receive Night output +- GIVEN either mode is active +- WHEN the user runs `dreamcoder light` or `dreamcoder dark` +- THEN the mode is persisted, the palette is validated, and every declared consumer receives that mode's output -#### Scenario: Night with a failing gate changes nothing +#### Scenario: A failing gate changes nothing -- GIVEN a transformed Night palette that fails a floor -- WHEN the user runs `dreamcoder night` -- THEN the command exits non-zero, no active output is changed, and the prior standard setting remains in effect +- GIVEN a palette that fails a floor +- WHEN the user runs `dreamcoder light` or `dreamcoder dark` +- THEN the command exits non-zero, no active output is changed, and the prior setting remains in effect -#### Scenario: Light and Dark exit Night +#### Scenario: Night is not a mode -- GIVEN `theme.render_profile=night` is active -- WHEN the user runs `dreamcoder light` or `dreamcoder dark` -- THEN `theme.render_profile=standard` is persisted before the base mode is applied, the standard identity is regenerated across the same target inventory, and subsequent sync uses the standard profile +- GIVEN the Night render profile was removed +- WHEN the user runs `dreamcoder night` or `dreamcoder theme apply night` +- THEN the command is rejected as unknown, exits non-zero, and changes nothing -#### Scenario: No automatic time-based activation +#### Scenario: Legacy render profile setting is tolerated -- GIVEN the Night profile is not persisted -- WHEN the theme scheduler runs at any time of day -- THEN it MUST NOT activate Night automatically and MUST keep the existing Light/Dark schedule +- GIVEN a persisted `theme.render_profile` from an older version +- WHEN settings are loaded, validated, or updated +- THEN loading succeeds with an unknown-setting warning and the key is preserved without affecting the mode ### Requirement: Blocking health verification -`scripts/verify-theme-health.py` MUST import canonical `contrast()` and `apca_lc()` from the package and MUST remove the `check_apca_or_warn()` advisory path for declared guardrail pairs. Every below-floor pair MUST terminate the command non-zero and report mode/profile, token or state pair, measured WCAG/APCA value, and required threshold. The command MUST validate standard Light, standard Dark, design-system Dusk, and derived Night deterministically, and MUST check Night before any generated artifact is accepted. It MUST declare generation/selection coverage for all 32 active consumers, and any consumer without declared coverage MUST block. `scripts/generate-theme-preview.py` MUST use the same canonical math and include Night measurements without creating screenshot baselines. +`scripts/verify-theme-health.py` MUST import canonical `contrast()` and `apca_lc()` from the package and MUST remove the `check_apca_or_warn()` advisory path for declared guardrail pairs. Every below-floor pair MUST terminate the command non-zero and report mode, token or state pair, measured WCAG/APCA value, and required threshold. The command MUST validate Light, Dark, and design-system Dusk deterministically. It MUST declare generation/selection coverage for every active consumer, and any consumer without declared coverage MUST block. `scripts/generate-theme-preview.py` MUST use the same canonical math without creating screenshot baselines. #### Scenario: Below-floor pair blocks with actionable diagnostics -- GIVEN a quiet-text pair measuring below Lc 44 in Night +- GIVEN a quiet-text pair measuring below Lc 44 in Dark - WHEN `verify-theme-health.py` runs -- THEN the command exits non-zero and reports profile Night, the pair, the measured Lc value, and the required threshold +- THEN the command exits non-zero and reports mode dark, the pair, the measured Lc value, and the required threshold -#### Scenario: All four profiles are validated +#### Scenario: All canonical palettes are validated -- GIVEN standard Light, standard Dark, design-system Dusk, and derived Night palettes +- GIVEN the Light, Dark, and design-system Dusk palettes - WHEN health validation runs -- THEN each profile is measured deterministically, and any below-floor pair in any profile blocks the command +- THEN each palette is measured deterministically, and any below-floor pair in any palette blocks the command #### Scenario: Advisory warnings become blocking @@ -196,13 +128,13 @@ The system MUST add a typed setting `theme.render_profile` to `SETTINGS_SCHEMA` #### Scenario: All-target coverage is declared -- GIVEN the Night profile +- GIVEN the sync coverage declaration - WHEN health validation runs -- THEN the report declares generation/selection coverage for all 32 active consumers, and any consumer without declared coverage blocks the command +- THEN it declares generation/selection coverage for every active consumer, and any consumer without declared coverage blocks the command ### Requirement: Focused regression coverage for the eye-comfort contract -The automated test suite MUST cover APCA known vectors, threshold boundaries, polarity, Night determinism, transform bounds, failed-transform no-write behavior, `theme.render_profile` setting validation, CLI activation, and all-target coverage. Advisory assertions and comments in `tests/test_dreamcoder_global_design_system.py` MUST be replaced with blocking checks. `tests/test_apca_implementation.py` MUST cross-validate the package `apca_lc()` against known vectors. Local and CI behavior MUST align around `python scripts/verify-theme-health.py` and the existing pytest suite. +The automated test suite MUST cover APCA known vectors, threshold boundaries, polarity, failed-gate no-write behavior, legacy `theme.render_profile` tolerance, Light/Dark CLI activation, and all-target coverage. Advisory assertions and comments in `tests/test_dreamcoder_global_design_system.py` MUST be replaced with blocking checks. `tests/test_apca_implementation.py` MUST cross-validate the package `apca_lc()` against known vectors. Local and CI behavior MUST align around `python scripts/verify-theme-health.py` and the existing pytest suite. #### Scenario: Known vectors cross-validate the package implementation @@ -216,21 +148,21 @@ The automated test suite MUST cover APCA known vectors, threshold boundaries, po - WHEN the focused tests run - THEN at-floor pairs pass and below-floor pairs fail with the correct metric and polarity-aware measurement -#### Scenario: Failed transform performs no writes +#### Scenario: Failed gate performs no writes -- GIVEN a test that forces the Night transform below a floor +- GIVEN a test that forces the dual gate to fail - WHEN the sync path runs under test - THEN no writes occur and the failure is asserted ### Requirement: Evidence and claims boundaries -Night MUST be documented as a user-controlled display profile, not a medical treatment. The system MUST NOT make blue-light-treatment, disease-prevention, eye-strain-cure, or sleep-improvement claims, MUST NOT add automatic warmth/color-temperature filtering, and MUST NOT replace the existing Hyprland 4000K keybindings, which remain a separate external display filter. `docs/DREAMCODER_DESIGN_SYSTEM.md` and generated preview policy MUST document WCAG 2.2 and APCA as independent blocking gates, and previously documented APCA exceptions MUST be corrected or explicitly removed rather than remaining accepted warnings. +The Light/Dark modes MUST be documented as display modes, not a medical treatment. The system MUST NOT make blue-light-treatment, disease-prevention, eye-strain-cure, or sleep-improvement claims, MUST NOT add automatic warmth/color-temperature filtering, and MUST NOT replace the existing Hyprland 4000K keybindings, which remain a separate external display filter. `docs/DREAMCODER_DESIGN_SYSTEM.md` and generated preview policy MUST document WCAG 2.2 and APCA as independent blocking gates, and previously documented APCA exceptions MUST be corrected or explicitly removed rather than remaining accepted warnings. -#### Scenario: Night is documented without treatment claims +#### Scenario: Modes are documented without treatment claims -- GIVEN the Night profile and its documentation +- GIVEN the Light/Dark documentation - WHEN the documentation and generated preview policy are inspected -- THEN Night is described as a validated luminance/chroma display profile with no medical-treatment, blue-light-filtering, or sleep claims, and no automatic warmth is introduced +- THEN the modes are described without medical-treatment, blue-light-filtering, or sleep claims, and no automatic warmth is introduced #### Scenario: Advisory APCA exceptions are removed diff --git a/openspec/specs/ml4w-keybindings-waybar/spec.md b/openspec/specs/ml4w-keybindings-waybar/spec.md index 95419cc5..f7924400 100644 --- a/openspec/specs/ml4w-keybindings-waybar/spec.md +++ b/openspec/specs/ml4w-keybindings-waybar/spec.md @@ -17,7 +17,7 @@ in this change makes Waybar run with it. The active-workspace accent highlight is also not reachable in the shipped chain — `style.css` imports only `colors.css`, which declares variables and no accent rule, while the `#workspaces button.active` accent rule lives in the engine-generated -`DreamcoderThemes/dreamcoder/waybar-{dark,light,night}.css`, which `style.css` +`DreamcoderThemes/dreamcoder/waybar-{dark,light}.css`, which `style.css` does not import. The delivered Waybar behavior is therefore the taskbar module plus `window-rewrite` icon mappings only. Both limitations, plus two further ones, are recorded non-normatively in `## Known Gaps` and MUST NOT be read as @@ -218,7 +218,7 @@ modules MUST carry its own configuration block. `DreamcoderWaybar/.config/waybar/style.css` MUST import exactly one stylesheet, `colors.css`, which is the Dreamcoder color bridge that selects the active -light/dark/night variable set written by the theme sync. The shipped import is +light/dark variable set written by the theme sync. The shipped import is `colors.css`; the `waybar-light.css` / `waybar-dark.css` files named by an earlier revision of this requirement were reconciled to the shipped import on 2026-09-11. The layout stylesheet MUST NOT define colors itself (no @@ -300,7 +300,7 @@ the layout config (see FR2.4). `scripts/apply-theme-mode.sh` - WHEN the changed paths and the Waybar section of that script are inspected - THEN no file under the theme engine package is present, and - `DreamcoderThemes/dreamcoder/waybar-{dark,light,night}.css` remain + `DreamcoderThemes/dreamcoder/waybar-{dark,light}.css` remain engine-generated and unedited - AND the mode switch still repoints `~/.config/waybar/colors.css` to the active mode variant before the sync and restarts Waybar @@ -375,7 +375,7 @@ satisfied requirements. `DreamcoderWaybar/.config/waybar/style.css` imports only `colors.css`, which declares variables and no accent rule; the `#workspaces button.active` accent rule lives in the engine-generated -`DreamcoderThemes/dreamcoder/waybar-{dark,light,night}.css`, which `style.css` +`DreamcoderThemes/dreamcoder/waybar-{dark,light}.css`, which `style.css` does not import. Delivered behavior is the module plus `window-rewrite` only. Follow-up: wire the mode CSS import so the accent rule reaches the live chain. diff --git a/openspec/specs/renderer-registry/spec.md b/openspec/specs/renderer-registry/spec.md index 75f89081..a4aa962e 100644 --- a/openspec/specs/renderer-registry/spec.md +++ b/openspec/specs/renderer-registry/spec.md @@ -94,7 +94,7 @@ registration would falsely assert that no active output exists. The `zellij` registration's `RepositoryStrategy.MODE_VARIANTS` claim MUST be resolved against one design-recorded decision: either (a) correct the claim -to match that only Night is generated today (orphaned `.kdl` status +to match the variants actually generated (orphaned `.kdl` status documented as a follow-up, never silently deleted), or (b) wire real dark/light `.kdl` generation so the existing claim becomes true. @@ -115,7 +115,7 @@ dark/light `.kdl` generation so the existing claim becomes true. ### Requirement: herdr registration represents all 3 supported profiles The `herdr` registration(s) MUST represent that -`sync_herdr_repo_variants()` writes dark/light(+night) variants for every +`sync_herdr_repo_variants()` writes dark/light variants for every `is_complete` profile in `SUPPORTED_PROFILES` (`herdr-0.7.3`, `-0.8.0`, `-0.8.2`), each under its own versioned directory. A single hardcoded profile/mode entry MUST NOT stand in for this fan-out. diff --git a/scripts/apply-theme-mode.sh b/scripts/apply-theme-mode.sh index 687e18e3..74ee196a 100755 --- a/scripts/apply-theme-mode.sh +++ b/scripts/apply-theme-mode.sh @@ -16,30 +16,11 @@ ML4W_CACHE_DIR="${ML4W_CACHE_DIR:-${CACHE_HOME}/ml4w/hyprland-dotfiles}" MODE="${1:-light}" WALLPAPER="${2:-${DREAMCODER_WALLPAPER:-}}" -PROFILE="${3:-${DREAMCODER_THEME_PROFILE:-standard}}" ML4W_WALLPAPER="${ML4W_CACHE_DIR}/current_wallpaper" [[ "${MODE}" == "light" || "${MODE}" == "dark" ]] || { printf 'Invalid mode: %s\n' "${MODE}" >&2 exit 1 } -case "${PROFILE}" in -standard | night) ;; -*) - printf 'Invalid render profile: %s (expected standard|night)\n' "${PROFILE}" >&2 - exit 1 - ;; -esac -# Night is an orthogonal render profile on top of the base mode: while -# DREAMCODER_THEME_MODE stays "dark", DREAMCODER_THEME_PROFILE=night selects -# the generated *-night artifacts instead of the standard dark ones. -VARIANT="${MODE}" -[[ "${PROFILE}" == "night" ]] && VARIANT="night" -# Night always resolves the Dreamcoder Dark base (ADR-003): a light -# base with profile=night is a conflict, never a silent coercion. -if [[ "${PROFILE}" == "night" && "${MODE}" != "dark" ]]; then - printf 'Invalid combination: render profile night requires base mode dark (got %s)\n' "${MODE}" >&2 - exit 1 -fi if [[ -z "${WALLPAPER}" && -f "${ML4W_WALLPAPER}" ]]; then WALLPAPER="$(cat "${ML4W_WALLPAPER}")"; fi CURSOR_CLI_ENV="${CACHE_HOME:-${HOME}/.cache}/dreamcoder/cursor-cli.env" @@ -48,37 +29,6 @@ light) CLI_COLORFGBG="0;15" ;; dark) CLI_COLORFGBG="15;0" ;; esac mkdir -p "$(dirname "${CURSOR_CLI_ENV}")" -# Preparation gate: never mutate symlinks or system mode until the Night -# plan is ready. For profile=night every repo-generated *-night artifact -# referenced by the selectors below must already exist (Phase 3 generation -# plus the Python validation-first gate are the other halves); a missing -# artifact aborts with ZERO mutations. -if [[ "${PROFILE}" == "night" ]]; then - REQUIRED_NIGHT_ARTIFACTS=( - "${DREAMCODER_DOTS_DIR}/DreamcoderKitty/.config/kitty/colors-dreamcoder-night.conf" - "${DREAMCODER_DOTS_DIR}/DreamcoderKitty/.config/kitty/dreamcoder-ui-night.conf" - "${DREAMCODER_DOTS_DIR}/DreamcoderGhostty/.config/ghostty/themes/dreamcoder-night" - "${DREAMCODER_DOTS_DIR}/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Night.yaml" - "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json" - "${DREAMCODER_DOTS_DIR}/DreamcoderZellij/.config/zellij/dreamcoder-night.kdl" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/hyprland-night.conf" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/hypr-colors-night.lua" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/hypr-colors-night.conf" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/waybar-night.css" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/rofi-night.rasi" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/btop-dreamcoder-night.theme" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/delta-dreamcoder-night.gitconfig" - "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/tmux-dreamcoder-night.conf" - "${DREAMCODER_DOTS_DIR}/DreamcoderLazygit/.config/lazygit/config.night.yml" - ) - for _artifact in "${REQUIRED_NIGHT_ARTIFACTS[@]}"; do - if [[ ! -f "${_artifact}" ]]; then - printf '✗ Night preparation failed: missing artifact %s\n' "${_artifact}" >&2 - printf ' Run repository Night generation first (DREAMCODER_THEME_PROFILE=night sync).\n' >&2 - exit 1 - fi - done -fi # Bounded preparation + settings persistence (control path). Runs only # when this script is the entry point (theme-auto / manual). When # invoked by the control transaction (DREAMCODER_SYNC_DONE=1), @@ -87,16 +37,13 @@ fi # (design §7/§8): system mode, symlink flips, and reloads run only # after preparation and settings persistence succeed. if [[ "${DREAMCODER_SYNC_DONE:-0}" != "1" ]]; then - _CONTROL_CHOICE="${MODE}" - [[ "${PROFILE}" == "night" ]] && _CONTROL_CHOICE="night" DREAMCODER_SYNC_DONE=1 \ DREAMCODER_THEME_MODE="${MODE}" \ - DREAMCODER_THEME_PROFILE="${PROFILE}" \ DREAMCODER_WALLPAPER="${WALLPAPER}" \ PYTHONPATH="${DREAMCODER_DOTS_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}" \ - python3 -m dreamcoder_theme.control theme apply "${_CONTROL_CHOICE}" --json >/dev/null + python3 -m dreamcoder_theme.control theme apply "${MODE}" --json >/dev/null fi -printf 'export COLORFGBG="%s"\nexport DREAMCODER_THEME_MODE="%s"\nexport DREAMCODER_THEME_PROFILE="%s"\nexport COLORTERM="truecolor"\nexport FORCE_COLOR="3"\nexport CLICOLOR_FORCE="1"\nunset NO_COLOR\n' "${CLI_COLORFGBG}" "${MODE}" "${PROFILE}" >"${CURSOR_CLI_ENV}" +printf 'export COLORFGBG="%s"\nexport DREAMCODER_THEME_MODE="%s"\nexport COLORTERM="truecolor"\nexport FORCE_COLOR="3"\nexport CLICOLOR_FORCE="1"\nunset NO_COLOR\n' "${CLI_COLORFGBG}" "${MODE}" >"${CURSOR_CLI_ENV}" "${DREAMCODER_DOTS_DIR}/scripts/apply-system-mode.sh" "${MODE}" if [[ -n "${WALLPAPER}" && -f "${WALLPAPER}" ]] && optional_command matugen; then @@ -109,26 +56,26 @@ fi # instead of overwriting the wrong one through a stale symlink. WAYBAR_COLORS="${HOME}/.config/waybar/colors.css" if [[ -L "${WAYBAR_COLORS}" ]]; then - ln -sf "colors-${VARIANT}.css" "${WAYBAR_COLORS}" + ln -sf "colors-${MODE}.css" "${WAYBAR_COLORS}" fi # --- Rofi: same treatment for colors.rasi symlink --- ROFI_COLORS="${HOME}/.config/rofi/colors.rasi" if [[ -L "${ROFI_COLORS}" ]]; then - ln -sf "colors-${VARIANT}.rasi" "${ROFI_COLORS}" + ln -sf "colors-${MODE}.rasi" "${ROFI_COLORS}" fi # --- Hyprland: flip colors.lua and colors.conf symlinks --- HYPR_LUA="${HOME}/.config/hypr/colors.lua" HYPR_CONF="${HOME}/.config/hypr/colors.conf" if [[ -L "${HYPR_LUA}" ]]; then - ln -sf "colors-${VARIANT}.lua" "${HYPR_LUA}" + ln -sf "colors-${MODE}.lua" "${HYPR_LUA}" fi if [[ -L "${HYPR_CONF}" ]]; then - ln -sf "colors-${VARIANT}.conf" "${HYPR_CONF}" + ln -sf "colors-${MODE}.conf" "${HYPR_CONF}" fi # --- Hyprland: flip dreamcoder-colors.lua --- HYPR_DC_LUA="${HOME}/.config/hypr/dreamcoder-colors.lua" if [[ -L "${HYPR_DC_LUA}" ]]; then - ln -sf "hypr-colors-${VARIANT}.lua" "${HYPR_DC_LUA}" + ln -sf "hypr-colors-${MODE}.lua" "${HYPR_DC_LUA}" fi # --- Kitty: flip colors-dreamcoder.conf and dreamcoder-ui.conf symlinks BEFORE sync --- # This ensures the Python sync writes to the correct variant file @@ -138,7 +85,7 @@ if [[ -d "${KITTY_DIR}" ]]; then for _link in colors-dreamcoder.conf dreamcoder-ui.conf; do _target="${KITTY_DIR}/${_link}" if [[ -L "${_target}" ]]; then - ln -sf "${_link%.conf}-${VARIANT}.conf" "${_target}" + ln -sf "${_link%.conf}-${MODE}.conf" "${_target}" fi done fi @@ -147,8 +94,8 @@ fi # --- Pi CLI: flip theme symlink --- PI_SCRIPT="${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" if [[ -f "${PI_SCRIPT}" ]]; then - DREAMCODER_THEME_MODE="${MODE}" DREAMCODER_THEME_PROFILE="${PROFILE}" bash "${PI_SCRIPT}" &>/dev/null || true - printf ' pi theme switched to %s mode (profile: %s)\n' "${MODE}" "${PROFILE}" + DREAMCODER_THEME_MODE="${MODE}" bash "${PI_SCRIPT}" &>/dev/null || true + printf ' pi theme switched to %s mode\n' "${MODE}" fi # The Python preparation + validation + settings persistence + commit ran @@ -170,15 +117,15 @@ if command -v tmux >/dev/null 2>&1; then fi # Update global environment so NEW panes/windows inherit the right vars tmux set-environment -g DREAMCODER_THEME_MODE "${MODE}" 2>/dev/null || true - tmux set-environment -g DREAMCODER_THEME_PROFILE "${PROFILE}" 2>/dev/null || true + # Drop the retired render-profile variable from long-lived servers. + tmux set-environment -gu DREAMCODER_THEME_PROFILE 2>/dev/null || true tmux set-environment -g COLORFGBG "${CLI_COLORFGBG}" 2>/dev/null || true - # Switch tmux-kanagawa theme variant to match Dreamcoder mode/profile + # Switch tmux-kanagawa theme variant to match the Dreamcoder mode # SOURCE OF TRUTH: colors MUST match DreamcoderThemes/dreamcoder/tokens.json - # When updating tokens, update BOTH light AND dark sections here (and - # night, derived through the canonical night transform in the package). + # When updating tokens, update BOTH light AND dark sections here. if [[ -d "${KANAGAWA_DIR}" ]]; then - case "${VARIANT}" in + case "${MODE}" in light) KANAGAWA_VARIANT="lotus" # Dreamcoder Light palette — source: tokens.json modes.light.{text,accent,error,...} @@ -217,26 +164,6 @@ if command -v tmux >/dev/null 2>&1; then tmux set-option -g @ukiyo-cpu-usage-colors "notice bg_bar" 2>/dev/null || true tmux set-option -g @ukiyo-ram-usage-colors "info bg_bar" 2>/dev/null || true ;; - night) - KANAGAWA_VARIANT="dragon" - # Dreamcoder Night profile — derived from Dreamcoder Dark in tokens.json modes.dark - # via the canonical night transform (brightness 0.86 / saturation 0.72). - tmux set-option -g @ukiyo-color-text "#c6c6c6" 2>/dev/null || true - tmux set-option -g @ukiyo-color-bg-bar "#0b0b0d" 2>/dev/null || true - tmux set-option -g @ukiyo-color-bg-pane "#090909" 2>/dev/null || true - tmux set-option -g @ukiyo-color-accent "#a4afee" 2>/dev/null || true - tmux set-option -g @ukiyo-color-info "#62b9e2" 2>/dev/null || true - tmux set-option -g @ukiyo-color-notice "#ababab" 2>/dev/null || true - tmux set-option -g @ukiyo-color-muted "#ababab" 2>/dev/null || true - tmux set-option -g @ukiyo-color-error "#e88484" 2>/dev/null || true - tmux set-option -g @ukiyo-color-alert "#d1a126" 2>/dev/null || true - tmux set-option -g @ukiyo-color-highlight "#3a76d6" 2>/dev/null || true - tmux set-option -g @ukiyo-color-selection "#323232" 2>/dev/null || true - # Swap plugin color order (same as dark: fg=bg_bar sobre bg=color) - tmux set-option -g @ukiyo-git-colors "accent bg_bar" 2>/dev/null || true - tmux set-option -g @ukiyo-cpu-usage-colors "notice bg_bar" 2>/dev/null || true - tmux set-option -g @ukiyo-ram-usage-colors "info bg_bar" 2>/dev/null || true - ;; esac tmux set-option -g @ukiyo-theme "kanagawa/${KANAGAWA_VARIANT}" 2>/dev/null || true # Reload plugin to apply new theme colors immediately @@ -261,11 +188,11 @@ fi # --- Herdr: switch config symlink + reload --- HERDR_SCRIPT="${DREAMCODER_DOTS_DIR}/scripts/herdr-theme-switch.sh" if [[ -f "${HERDR_SCRIPT}" ]]; then - bash "${HERDR_SCRIPT}" "${VARIANT}" + bash "${HERDR_SCRIPT}" "${MODE}" fi # --- /Herdr --- -printf '✓ Dreamcoder %s mode applied (profile: %s)\n' "${MODE}" "${PROFILE}" +printf '✓ Dreamcoder %s mode applied\n' "${MODE}" # --- Post-sync: fix any stale symlinks --- # Waybar colors.css, Rofi colors.rasi, and Hyprland colors.lua/colors.conf @@ -275,12 +202,12 @@ printf '✓ Dreamcoder %s mode applied (profile: %s)\n' "${MODE}" "${PROFILE}" DOTS_DIR="${DREAMCODER_DOTS_DIR:-${HOME}/Documents/PROYECTOS/dreamcoder-dots}" DUNST_CONF="${HOME}/.config/dunst/dreamcoder-dunst.conf" WARP_THEME="${HOME}/.local/share/warp-terminal/themes/Dreamcoder.yaml" -WARP_VARIANT="${DOTS_DIR}/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-${VARIANT^}.yaml" +WARP_VARIANT="${DOTS_DIR}/DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-${MODE^}.yaml" if [[ -L "${DUNST_CONF}" ]]; then target=$(readlink "${DUNST_CONF}") case "${target}" in - *dunst-dreamcoder-dark.conf | *dunst-dreamcoder-light.conf | *dunst-dreamcoder-night.conf) + *dunst-dreamcoder-dark.conf | *dunst-dreamcoder-light.conf) ln -sf "${DOTS_DIR}/DreamcoderThemes/dreamcoder/dunst-dreamcoder.conf" "${DUNST_CONF}" ;; esac @@ -292,7 +219,7 @@ fi # Btop: flip theme symlink to variant-specific artifact BTOP_THEME="${HOME}/.config/btop/themes/dreamcoder.theme" if [[ -L "${BTOP_THEME}" ]]; then - ln -sf "dreamcoder-${VARIANT}.theme" "${BTOP_THEME}" + ln -sf "dreamcoder-${MODE}.theme" "${BTOP_THEME}" fi # btop.conf is ML4W-owned and upgrades reset color_theme (to "matugen"), so # re-select the Dreamcoder theme on every apply; a no-op when already set. @@ -304,12 +231,12 @@ fi # Zellij: update theme in config.kdl ZELLIJ_CONF="${HOME}/.config/zellij/config.kdl" if [[ -f "${ZELLIJ_CONF}" ]]; then - sed -i "s/^theme \".*\"/theme \"dreamcoder-${VARIANT}\"/" "${ZELLIJ_CONF}" + sed -i "s/^theme \".*\"/theme \"dreamcoder-${MODE}\"/" "${ZELLIJ_CONF}" fi # Delta: flip git diff theme symlink to variant-specific artifact DELTA_LINK="${HOME}/.config/git/delta-dreamcoder.gitconfig" -DELTA_VARIANT="${DOTS_DIR}/DreamcoderThemes/dreamcoder/delta-dreamcoder-${VARIANT}.gitconfig" +DELTA_VARIANT="${DOTS_DIR}/DreamcoderThemes/dreamcoder/delta-dreamcoder-${MODE}.gitconfig" if [[ -f "${DELTA_VARIANT}" ]]; then ln -sf "${DELTA_VARIANT}" "${DELTA_LINK}" fi @@ -320,7 +247,7 @@ fi # repo keeps the generated active config.yml (mode-tracking) for COPY-style # installs. ln -sf replaces the link atomically. LAZYGIT_LINK="${HOME}/.config/lazygit/config.yml" -LAZYGIT_VARIANT="${DOTS_DIR}/DreamcoderLazygit/.config/lazygit/config.${VARIANT}.yml" +LAZYGIT_VARIANT="${DOTS_DIR}/DreamcoderLazygit/.config/lazygit/config.${MODE}.yml" if [[ -f "${LAZYGIT_VARIANT}" ]]; then ln -sf "${LAZYGIT_VARIANT}" "${LAZYGIT_LINK}" fi diff --git a/scripts/dreamcoder b/scripts/dreamcoder index 3ad26059..8957fa6e 100755 --- a/scripts/dreamcoder +++ b/scripts/dreamcoder @@ -37,14 +37,13 @@ case "${CMD}" in upstream-diff) exec "${DREAMCODER_DOTS_DIR}/scripts/upstream-diff.py" "$@" ;; preview) exec "${DREAMCODER_DOTS_DIR}/scripts/generate-theme-preview.py" "$@" ;; auto) exec "${DREAMCODER_DOTS_DIR}/scripts/theme-auto.sh" "$@" ;; - # Phase 5 (R7): light/dark/night route through the control activation - # path (prepare -> validate -> persist -> commit -> reload adapter); the - # generic settings interface continues to serve theme.render_profile. - light|dark|night) CONTROL theme apply "${CMD}" "$@" ;; + # Phase 5 (R7): light/dark route through the control activation path + # (prepare -> validate -> persist -> commit -> reload adapter). + light|dark) CONTROL theme apply "${CMD}" "$@" ;; theme) CONTROL theme "$@" ;; sync) exec "${DREAMCODER_DOTS_DIR}/scripts/sync-dreamcoder-theme.py" "$@" ;; *) - printf 'Usage: dreamcoder {install|repair|doctor|doctor-json|dashboard|tui|docs|audit|control|settings|profile|motion|backup|visual|installer|status|verify|upstream-diff|preview|auto|light|dark|night|theme|sync}\n' >&2 + printf 'Usage: dreamcoder {install|repair|doctor|doctor-json|dashboard|tui|docs|audit|control|settings|profile|motion|backup|visual|installer|status|verify|upstream-diff|preview|auto|light|dark|theme|sync}\n' >&2 exit 2 ;; esac diff --git a/scripts/generate-theme-preview.py b/scripts/generate-theme-preview.py index 8603fb47..bd7aeafe 100755 --- a/scripts/generate-theme-preview.py +++ b/scripts/generate-theme-preview.py @@ -9,7 +9,6 @@ sys.path.insert(0, str(SRC)) from dreamcoder_theme._math import apca_lc # noqa: E402 -from dreamcoder_theme.palette import night_palette # noqa: E402 TOKENS = ROOT / "DreamcoderThemes/dreamcoder/tokens.json" OUT = ROOT / "docs/generated/dreamcoder-theme-preview.md" @@ -178,21 +177,6 @@ def main(): parts.append("") parts.append(ui_contrast_table(label, palette)) parts.append("") - night_params = tokens.get("render_profiles", {}).get("night", {}) - try: - guardrail_numbers: dict[str, float] = { - k: float(v) for k, v in guardrails.items() if isinstance(v, (int, float)) - } - night = night_palette(tokens["modes"]["dark"], night_params, guardrail_numbers) - except (KeyError, TypeError, ValueError) as error: - raise SystemExit(f"unable to derive Night render profile: {error}") from error - night_label = "Night (render profile derived from Dreamcoder Dark)" - parts.append(contrast_table(night_label, night)) - parts.append("") - parts.append(apca_table(night_label, night, body_min, ui_min)) - parts.append("") - parts.append(ui_contrast_table(night_label, night)) - parts.append("") parts += [ "## Usage", "", diff --git a/scripts/theme-auto.sh b/scripts/theme-auto.sh index 0962e5da..a0ea4a6c 100755 --- a/scripts/theme-auto.sh +++ b/scripts/theme-auto.sh @@ -11,8 +11,6 @@ DARK_START="${DREAMCODER_DARK_START:-18}" HOUR="$(date +%H)" MODE="dark" if (( 10#${HOUR} >= LIGHT_START && 10#${HOUR} < DARK_START )); then MODE="light"; fi -# The scheduler keeps its existing Light/Dark schedule and never activates -# Night (R7, task 5.6): the render profile is pinned to standard regardless -# of any ambient DREAMCODER_THEME_PROFILE, and the bounded adapter routes -# through the control activation path. -exec "${DREAMCODER_DOTS_DIR}/scripts/apply-theme-mode.sh" "${MODE}" "${DREAMCODER_WALLPAPER:-}" "standard" +# Light/Dark schedule; the bounded adapter routes through the control +# activation path. +exec "${DREAMCODER_DOTS_DIR}/scripts/apply-theme-mode.sh" "${MODE}" "${DREAMCODER_WALLPAPER:-}" diff --git a/scripts/verify-theme-health.py b/scripts/verify-theme-health.py index 2905e391..78f45964 100755 --- a/scripts/verify-theme-health.py +++ b/scripts/verify-theme-health.py @@ -20,7 +20,7 @@ load_tokens, ) from dreamcoder_theme.palette import ansi as terminal_ansi # noqa: E402 -from dreamcoder_theme.palette import load_guardrails, night_palette, validate_palette # noqa: E402 +from dreamcoder_theme.palette import validate_palette # noqa: E402 from dreamcoder_theme.renderers_opencode import opencode_content # noqa: E402 FILES = [ @@ -31,7 +31,6 @@ ANTIGRAVITY_FILES = [ ROOT / "DreamcoderAntigravity/Dreamcoder.json", ROOT / "DreamcoderAntigravity/Dreamcoder-Dark.json", - ROOT / "DreamcoderAntigravity/Dreamcoder-Night.json", ROOT / "DreamcoderAntigravity/Dreamcoder-Light.json", ] TOKEN_FILE = ROOT / "DreamcoderThemes/dreamcoder/tokens.json" @@ -376,10 +375,8 @@ def check_tokens(): def check_dual_gate_candidates(): - """Validate the package dual gate on all four deterministic candidates: - standard Light, standard Dark, design-system Dusk, and derived Night - (Night = night_palette of canonical dark with canonical render_profiles, - wallpaper adaptation disabled for the gate). Any dual-gate error blocks.""" + """Validate the package dual gate on all three deterministic candidates: + Light, Dark, and design-system Dusk. Any dual-gate error blocks.""" tokens = load_tokens(TOKEN_FILE) # float() narrows the comprehension to dict[str, float] (mirrors # palette.load_guardrails), satisfying the dict-invariance typing. @@ -389,24 +386,18 @@ def check_dual_gate_candidates(): } except (TypeError, ValueError) as error: raise SystemExit(f"invalid numeric guardrail: {error}") from error - night_params = tokens.get("render_profiles", {}).get("night") - require( - isinstance(night_params, dict), - "tokens: render_profiles.night missing (canonical Night parameters required)", - ) candidates = [ ("light", tokens["modes"]["light"], "light"), ("dark", tokens["modes"]["dark"], "dark"), ("dusk", tokens["modes"]["dusk"], "dusk"), - ("night", night_palette(tokens["modes"]["dark"], night_params, guardrails), "dark"), ] for label, palette, mode in candidates: - errors = validate_palette(palette, guardrails, profile=label, mode=mode) + errors = validate_palette(palette, guardrails, mode=mode) require(not errors, f"dual gate {label}:\n" + "\n".join(errors)) -def check_night_coverage(): - """Fail when the sync 33-consumer Night coverage declaration is missing, +def check_coverage_declaration(): + """Fail when the sync 33-consumer coverage declaration is missing, duplicated, or not exactly the design matrix's 33 IDs.""" from dreamcoder_theme.sync import COVERAGE @@ -417,18 +408,14 @@ def check_night_coverage(): def check_antigravity_files(): - """Validate Antigravity's semantic button pair for every render profile.""" + """Validate Antigravity's semantic button pair for every mode file.""" if not any(file.exists() for file in ANTIGRAVITY_FILES): return tokens = load_tokens(TOKEN_FILE) - guardrails = load_guardrails(TOKEN_FILE) palettes = { "Dreamcoder.json": tokens["modes"]["dark"], "Dreamcoder-Dark.json": tokens["modes"]["dark"], "Dreamcoder-Light.json": tokens["modes"]["light"], - "Dreamcoder-Night.json": night_palette( - tokens["modes"]["dark"], tokens["render_profiles"]["night"], guardrails - ), } for file in ANTIGRAVITY_FILES: colors = _load_json(file)["colors"] @@ -573,17 +560,6 @@ def _health_findings(): for mode in ("dark", "light", "dusk") if mode in tokens["modes"] } - guardrails = { - key: float(value) - for key, value in tokens["guardrails"].items() - if isinstance(value, int | float) - } - expected_by_mode["night"] = opencode_content( - night_palette( - tokens["modes"]["dark"], tokens["render_profiles"]["night"], guardrails - ), - transparent_background=True, - ) if actual not in set(expected_by_mode.values()): findings.append( "STALE_ARTIFACT: .opencode/themes/dreamcoder.json " @@ -724,7 +700,7 @@ def check_hypr_colors_file(file): check_opencode_repo() check_design_system_contract() check_dual_gate_candidates() -check_night_coverage() +check_coverage_declaration() check_antigravity_files() for file in KITTY_FILES: check_kitty_colors(file) diff --git a/skills/dreamcoder-palette-tokens/SKILL.md b/skills/dreamcoder-palette-tokens/SKILL.md index 893f9b9e..f87f49b6 100644 --- a/skills/dreamcoder-palette-tokens/SKILL.md +++ b/skills/dreamcoder-palette-tokens/SKILL.md @@ -12,8 +12,7 @@ Defined in `DreamcoderThemes/dreamcoder/tokens.json` with validation schema at ` | `light` | Dreamcoder Light | `#f3eadc` | `#824f16` | 07:00-16:00 | | `dusk` | Dreamcoder Dusk | `#ebe4d6` | `#8a5520` | 16:00-18:00 | -These are the only canonical modes. `night` is a render profile derived from Dreamcoder Dark, -not a schema mode. OLED behavior belongs to `modes.dark.surface_policy`. +These are the only canonical modes; Dark and Light are the user-facing ones. OLED behavior belongs to `modes.dark.surface_policy`. ## Token Categories diff --git a/src/dreamcoder_theme/cli_handlers.py b/src/dreamcoder_theme/cli_handlers.py index 22f623c7..c50684ac 100644 --- a/src/dreamcoder_theme/cli_handlers.py +++ b/src/dreamcoder_theme/cli_handlers.py @@ -249,12 +249,8 @@ def handle_backup(args: argparse.Namespace) -> int: # Theme activation transaction (design §7/§8, R4/R7; tasks 5.3/5.5) # --------------------------------------------------------------------------- -# User choice -> (base mode, render profile) per design §7 table. -THEME_CHOICES: dict[str, tuple[str, str]] = { - "light": ("light", "standard"), - "dark": ("dark", "standard"), - "night": ("dark", "night"), -} +# Activatable theme modes (design §7 table): Dreamcoder Light and Dark. +THEME_CHOICES: tuple[str, ...] = ("light", "dark") class ThemeActivationError(RuntimeError): @@ -411,18 +407,14 @@ def _mutable_paths(paths: Any) -> list[Path]: return mutable -def _bridge_variant(base: str, profile: str) -> str: - return "night" if profile == "night" else base - - -def _flip_bridge_symlinks(paths: Any, base: str, profile: str) -> None: +def _flip_bridge_symlinks(paths: Any, base: str) -> None: """Select the correct variant target for matugen bridge symlinks (design §6). Only flips paths that are already symlinks — regular files are written directly by the sync writers. The activation transaction snapshots these before the flip, so a rollback restores the prior link targets exactly. """ - variant = _bridge_variant(base, profile) + variant = base bridges = [ (paths.waybar_matugen, f"colors-{variant}.css"), (paths.rofi_matugen, f"colors-{variant}.rasi"), @@ -437,7 +429,7 @@ def _flip_bridge_symlinks(paths: Any, base: str, profile: str) -> None: os.symlink(target, link) -def run_reload_adapter(base: str, profile: str) -> None: +def run_reload_adapter(base: str) -> None: """Invoke the bounded post-validation system/reload adapter (design §7). ``scripts/apply-theme-mode.sh`` owns the system-mode, symlink, and reload @@ -453,12 +445,11 @@ def run_reload_adapter(base: str, profile: str) -> None: env.update( { "DREAMCODER_THEME_MODE": base, - "DREAMCODER_THEME_PROFILE": profile, "DREAMCODER_SYNC_DONE": "1", } ) proc = subprocess.run( - [str(script), base, env.get("DREAMCODER_WALLPAPER", ""), profile], + [str(script), base, env.get("DREAMCODER_WALLPAPER", "")], env=env, text=True, stdout=subprocess.PIPE, @@ -471,8 +462,8 @@ def run_reload_adapter(base: str, profile: str) -> None: ) -def _regenerate_prior(paths: Any, prior_base: str, prior_profile: str) -> None: - """Best-effort regeneration of the prior profile after a rollback. +def _regenerate_prior(paths: Any, prior_base: str) -> None: + """Best-effort regeneration of the prior mode after a rollback. Snapshots already restore every mutable path byte-for-byte; regeneration is a safety net for deterministic repo artifacts and never masks the original @@ -481,8 +472,8 @@ def _regenerate_prior(paths: Any, prior_base: str, prior_profile: str) -> None: # Best-effort by contract: any regeneration failure must not mask the # original activation error this rollback is recovering from. with contextlib.suppress(Exception): - prepared = sync.prepare(prior_base, prior_profile) - sync.sync_active_targets(paths, prepared.active, prior_base, prior_profile) + prepared = sync.prepare(prior_base) + sync.sync_active_targets(paths, prepared.active, prior_base) sync.sync_bat_theme_variants(paths, prepared.variants) if write_repo_enabled(): sync.sync_repo_snippets(prepared.variants, prepared.active) @@ -492,7 +483,6 @@ def _commit_activation( paths: Any, prepared: sync.PreparedSync, base: str, - profile: str, changed: dict[str, bool], ) -> None: """Commit one prepared activation: persist, flip bridges, write, validate. @@ -501,16 +491,14 @@ def _commit_activation( (write failure, invalid post-write selector, blocking reload failure) triggers a full snapshot rollback in the caller. """ - # 4. Persist settings before applying the base mode (R7: light/dark - # explicitly persist standard and exit Night). + # 4. Persist settings before applying the base mode (R7). set_nested_setting("terminal.default_mode", base) - set_nested_setting("theme.render_profile", profile) # 5. Flip matugen bridge symlinks to the target variant before the writers - # run (design §6: "select colors-night.css before commit"). - _flip_bridge_symlinks(paths, base, profile) + # run (design §6). + _flip_bridge_symlinks(paths, base) # 6. Commit variants, active files, and selectors. write_if_changed() # semantics are preserved — each entry reports bool changed. - changed.update(sync.sync_active_targets(paths, prepared.active, base, profile)) + changed.update(sync.sync_active_targets(paths, prepared.active, base)) bat_changes = sync.sync_bat_theme_variants(paths, prepared.variants) if write_repo_enabled(): repo_changes = sync.sync_repo_snippets(prepared.variants, prepared.active) @@ -521,31 +509,30 @@ def _commit_activation( raise ThemeActivationError(f"Generated Starship config is invalid: {paths.starship}") # 8. Bounded system/reload adapter; a blocking reload failure rolls back. if os.environ.get("DREAMCODER_SYNC_DONE") != "1": - run_reload_adapter(base, profile) + run_reload_adapter(base) -def apply_theme(base: str, profile: str, choice: str, as_json: bool) -> int: - """Activate a (base, profile) pair as one transaction (design §7/§8). +def apply_theme(base: str, as_json: bool) -> int: + """Activate a Light/Dark base mode as one transaction (design §7/§8). Order: prepare+validate (zero writes) -> snapshot every mutable active path + selector file + settings -> persist settings -> flip bridge symlinks -> commit variants/active files/selectors -> post-write validation -> bounded system/reload adapter. Any exception, invalid post-write selector, blocking reload failure, or incomplete coverage restores snapshots and prior settings - and regenerates the prior profile before returning non-zero. + and regenerates the prior mode before returning non-zero. """ paths = theme_paths() # 1. Prepare + validate: a failed gate exits before any persistence, write, # symlink change, cleanup, system-mode change, or reload (R4/R8). try: - prepared = sync.prepare(base, profile) + prepared = sync.prepare(base) except sync.ThemeGateError as exc: if as_json: emit( { - "requested": choice, + "requested": base, "effective_base": base, - "effective_profile": profile, "coverage": "0/0", "changed": {}, "rollback_state": "rejected", @@ -559,9 +546,7 @@ def apply_theme(base: str, profile: str, choice: str, as_json: bool) -> int: # 2. Prior persisted state (the rollback target). prior_base = settings_get("terminal.default_mode") - prior_profile = settings_get("theme.render_profile") prior_base = prior_base if prior_base in {"light", "dark"} else "light" - prior_profile = prior_profile if prior_profile in {"standard", "night"} else "standard" # 3. Snapshot every mutable active path + selector file BEFORE the first # mutation (design §4: "snapshot settings and every mutable active path"). @@ -585,19 +570,17 @@ def apply_theme(base: str, profile: str, choice: str, as_json: bool) -> int: rollback_state = "none" changed: dict[str, bool] = {} try: - _commit_activation(paths, prepared, base, profile, changed) + _commit_activation(paths, prepared, base, changed) except Exception as exc: rollback_state = "restored" _restore_snapshots(snapshots, dir_captures) set_nested_setting("terminal.default_mode", prior_base) - set_nested_setting("theme.render_profile", prior_profile) - _regenerate_prior(paths, prior_base, prior_profile) + _regenerate_prior(paths, prior_base) if as_json: emit( { - "requested": choice, + "requested": base, "effective_base": prior_base, - "effective_profile": prior_profile, "coverage": f"{len(prepared.coverage)}/{len(prepared.coverage)}", "changed": {}, "rollback_state": rollback_state, @@ -611,9 +594,8 @@ def apply_theme(base: str, profile: str, choice: str, as_json: bool) -> int: return 1 status = { - "requested": choice, + "requested": base, "effective_base": base, - "effective_profile": profile, "coverage": f"{len(prepared.coverage)}/{len(prepared.coverage)}", "changed": changed, "rollback_state": rollback_state, @@ -632,8 +614,7 @@ def _restore_snapshots( def handle_theme(args: argparse.Namespace) -> int: - """Activate a theme base mode + render profile (design §7).""" - if args.theme_cmd != "apply": + """Activate the Dreamcoder Light or Dark theme (design §7).""" + if args.theme_cmd != "apply" or args.choice not in THEME_CHOICES: return 2 - base, profile = THEME_CHOICES[args.choice] - return apply_theme(base, profile, args.choice, args.json) + return apply_theme(args.choice, args.json) diff --git a/src/dreamcoder_theme/cli_parser.py b/src/dreamcoder_theme/cli_parser.py index f77b95b7..31d823aa 100644 --- a/src/dreamcoder_theme/cli_parser.py +++ b/src/dreamcoder_theme/cli_parser.py @@ -112,11 +112,9 @@ def build_parser() -> argparse.ArgumentParser: backup_restore.add_argument("--dry-run", action="store_true") backup_restore.add_argument("--json", action="store_true") - theme = sub.add_parser("theme", help="Activate a theme base mode and render profile") + theme = sub.add_parser("theme", help="Activate the Dreamcoder Light or Dark theme") theme_sub = theme.add_subparsers(dest="theme_cmd", required=True) theme_apply = theme_sub.add_parser("apply", help="Apply a theme choice as one transaction") - theme_apply.add_argument( - "choice", choices=["light", "dark", "night"], help="Theme choice to activate" - ) + theme_apply.add_argument("choice", choices=["light", "dark"], help="Theme choice to activate") theme_apply.add_argument("--json", action="store_true") return parser diff --git a/src/dreamcoder_theme/doctor.py b/src/dreamcoder_theme/doctor.py index 3a008fb3..0b4a3ed2 100644 --- a/src/dreamcoder_theme/doctor.py +++ b/src/dreamcoder_theme/doctor.py @@ -285,7 +285,7 @@ def doctor_checks() -> list[HealthCheck]: timer_status = "ok" if result.returncode == 0 and timer_detail == "active" else "warn" checks.append( HealthCheck( - name="day/night timer", + name="light/dark timer", status=timer_status, detail=timer_detail, repair="systemctl --user enable --now dreamcoder-theme-auto.timer", diff --git a/src/dreamcoder_theme/herdr-contract-evidence.md b/src/dreamcoder_theme/herdr-contract-evidence.md index a1a4ce31..96429b57 100644 --- a/src/dreamcoder_theme/herdr-contract-evidence.md +++ b/src/dreamcoder_theme/herdr-contract-evidence.md @@ -155,7 +155,7 @@ from 0.9.0), binary SHA-256 `herdr config check` rejects unknown keys (`unknown config key theme.custom.; ignoring key`) and unknown theme names with a non-zero exit, which closes the open question of whether the 0.8.2 field set is still -accepted: each generated `0.9.1/config.{dark,light,night}.toml` passed +accepted: each generated `0.9.1/config.{dark,light}.toml` passed `HERDR_CONFIG_PATH= herdr config check` with `config: ok`. The profile therefore reuses the 0.8.2 theme, custom, and ui field sets exactly and adds no new fields. diff --git a/src/dreamcoder_theme/herdr_switch.py b/src/dreamcoder_theme/herdr_switch.py index bf400d3d..f5db3945 100644 --- a/src/dreamcoder_theme/herdr_switch.py +++ b/src/dreamcoder_theme/herdr_switch.py @@ -31,12 +31,12 @@ _VERSION_TIMEOUT_SECONDS = 5 _RELOAD_TIMEOUT_SECONDS = 10 -_VALID_MODES = ("dark", "light", "night") +_VALID_MODES = ("dark", "light") _VARIANT_ROOT = ROOT / "DreamcoderHerdr/.config/herdr/dreamcoder" _VERSION_OUTPUT = re.compile(r"herdr[ \t]+(\d+)\.(\d+)\.(\d+)", re.IGNORECASE) Run = Callable[..., subprocess.CompletedProcess[str]] -Mode = Literal["dark", "light", "night"] +Mode = Literal["dark", "light"] Status = Literal[ "applied", diff --git a/src/dreamcoder_theme/palette.py b/src/dreamcoder_theme/palette.py index 4ff7f255..7c304376 100644 --- a/src/dreamcoder_theme/palette.py +++ b/src/dreamcoder_theme/palette.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import math import re import subprocess import warnings @@ -32,9 +31,7 @@ "guard", "hex_to_rgb", "load_guardrails", - "load_render_profile", "mix", - "night_palette", "rel_luminance", "rgb_to_hex", "surface_guard", @@ -206,7 +203,7 @@ def make_guard(palette: dict[str, str], minimum: float = 3.0) -> Callable[[str], # Declarative APCA pair classes (ADR-002). # # Each class names its token pairs and the guardrail keys that own the -# threshold — light/dusk floor and dark/night floor. No numeric policy +# threshold — light/dusk floor and dark floor. No numeric policy # literals are allowed here; a missing guardrail key fails validation. # ------------------------------------------------------------------ _APCA_PAIR_CLASSES: tuple[tuple[str, tuple[tuple[str, str], ...], str, str], ...] = ( @@ -263,7 +260,6 @@ def validate_palette( palette: dict[str, str], guardrails: dict[str, float] | None = None, *, - profile: str = "standard", mode: str | None = None, ) -> list[str]: """Return stable validation errors for a mode palette. @@ -273,13 +269,12 @@ def validate_palette( failure on the other. Thresholds are resolved from ``guardrails`` by key; APCA thresholds must be present or validation fails closed. - ``profile`` is the rendering profile (``standard`` today; ``night`` - arrives with the transform). ``mode`` defaults to the palette-derived - base mode (``dark`` for ``details=darker``, otherwise ``light``). + ``mode`` defaults to the palette-derived base mode (``dark`` for + ``details=darker``, otherwise ``light``). Metric diagnostics use the stable shape:: - {metric} fail: mode={mode} profile={profile} pair={fg}/{bg} \ + {metric} fail: mode={mode} pair={fg}/{bg} \ measured={value} guardrail={key}={threshold} """ g = guardrails or {} @@ -292,14 +287,14 @@ def validate_palette( def wcag_diag(fg_key: str, bg_key: str, measured: float, key: str, threshold: float) -> str: return ( - f"WCAG fail: mode={effective_mode} profile={profile} " + f"WCAG fail: mode={effective_mode} " f"pair={fg_key}/{bg_key} measured={measured:.2f} " f"guardrail={key}={threshold}" ) def apca_diag(cls: str, fg_key: str, bg_key: str, lc: float, key: str, threshold: float) -> str: return ( - f"APCA fail: mode={effective_mode} profile={profile} " + f"APCA fail: mode={effective_mode} " f"pair={fg_key}/{bg_key} class={cls} measured={abs(lc):.1f} " f"guardrail={key}={threshold}" ) @@ -377,28 +372,7 @@ def apca_diag(cls: str, fg_key: str, bg_key: str, lc: float, key: str, threshold return errors -# --------------------------------------------------------------------------- -# Canonical Night/Dim rendering profile (Phase 2; ADR-003). -# -# The transform is a deterministic brightness/saturation reduction of the -# Dreamcoder Dark palette. All parameters come from the canonical token -# contract (render_profiles.night) — never policy literals — and the bounded -# corrective pass restores a floor without weakening any threshold or -# brightening background/surface roles. Failure after the bound is reported by -# the caller's validate_palette() gate (fail closed, no standard-dark fallback). -# --------------------------------------------------------------------------- - -_NIGHT_PROFILE_KEYS = ( - "brightness_factor", - "saturation_factor", - "maximum_corrective_delta", - "corrective_step", -) - -_HEX_RE = re.compile(r"^#[0-9a-fA-F]{6}$") -_RGBA_RE = re.compile(r"^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([^)]+)\)$") - -# Guardrail keys validate_palette() / night_palette() resolve; a missing key +# Guardrail keys validate_palette() resolves; a missing key # fails closed so thresholds always originate in the canonical token file. _REQUIRED_GUARDRAIL_KEYS = ( "minimum_text_contrast", @@ -422,27 +396,6 @@ def _require_guardrails(guardrails: dict[str, float]) -> None: raise ValueError(f"missing required guardrails: {', '.join(missing)}") -def _validate_profile_parameters(params: dict[str, float]) -> None: - """Fail closed when render-profile parameters violate the canonical bounds.""" - if set(params) != set(_NIGHT_PROFILE_KEYS): - raise ValueError( - "night profile parameters must contain exactly " - f"{sorted(_NIGHT_PROFILE_KEYS)}, got {sorted(params)}" - ) - brightness = params["brightness_factor"] - saturation = params["saturation_factor"] - max_delta = params["maximum_corrective_delta"] - step = params["corrective_step"] - if not (0 < brightness <= 1): - raise ValueError("brightness_factor must be in (0, 1]") - if not (0 < saturation <= 1): - raise ValueError("saturation_factor must be in (0, 1]") - if not (0 <= max_delta <= 0.20): - raise ValueError("maximum_corrective_delta must be in [0, 0.20]") - if not (0 < step <= max_delta): - raise ValueError("corrective_step must be in (0, maximum_corrective_delta]") - - def load_guardrails(tokens_file: Path) -> dict[str, float]: """Load numeric guardrails from the canonical token file, failing closed. @@ -460,282 +413,3 @@ def load_guardrails(tokens_file: Path) -> dict[str, float]: numeric = {k: float(v) for k, v in guardrails.items() if isinstance(v, (int, float))} _require_guardrails(numeric) return numeric - - -def load_render_profile(tokens_file: Path, name: str = "night") -> dict[str, float]: - """Load canonical render-profile parameters, failing closed on absence.""" - if not tokens_file.is_file(): - raise ValueError(f"tokens file not found: {tokens_file}") - try: - tokens = json.loads(tokens_file.read_text(encoding="utf-8")) - except (json.JSONDecodeError, OSError) as exc: - raise ValueError(f"invalid tokens file: {tokens_file}") from exc - profile = tokens.get("render_profiles", {}).get(name) - if not isinstance(profile, dict): - raise ValueError(f"render profile {name!r} missing from tokens") - params = {k: float(v) for k, v in profile.items() if isinstance(v, (int, float))} - _validate_profile_parameters(params) - return params - - -def _hsl_from_rgb(r: int, g: int, b: int) -> tuple[float, float, float]: - rn, gn, bn = r / 255, g / 255, b / 255 - mx, mn = max(rn, gn, bn), min(rn, gn, bn) - lightness = (mx + mn) / 2 - delta = mx - mn - if delta == 0: - return 0.0, 0.0, lightness - saturation = delta / (1 - abs(2 * lightness - 1)) - if mx == rn: - hue = 60 * (((gn - bn) / delta) % 6) - elif mx == gn: - hue = 60 * ((bn - rn) / delta + 2) - else: - hue = 60 * ((rn - gn) / delta + 4) - return hue, saturation, lightness - - -def _rgb_from_hsl(hue: float, saturation: float, lightness: float) -> tuple[int, int, int]: - chroma = (1 - abs(2 * lightness - 1)) * saturation - x = chroma * (1 - abs((hue / 60) % 2 - 1)) - m = lightness - chroma / 2 - if hue < 60: - r1, g1, b1 = chroma, x, 0.0 - elif hue < 120: - r1, g1, b1 = x, chroma, 0.0 - elif hue < 180: - r1, g1, b1 = 0.0, chroma, x - elif hue < 240: - r1, g1, b1 = 0.0, x, chroma - elif hue < 300: - r1, g1, b1 = x, 0.0, chroma - else: - r1, g1, b1 = chroma, 0.0, x - # Deterministic nearest-integer rounding for byte-identical output. - r = round((r1 + m) * 255) - g = round((g1 + m) * 255) - b = round((b1 + m) * 255) - return max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b)) - - -def _transform_hex(value: str, params: dict[str, float]) -> str: - hue, saturation, lightness = _hsl_from_rgb(*hex_to_rgb(value)) - rgb = _rgb_from_hsl( - hue, - saturation * params["saturation_factor"], - lightness * params["brightness_factor"], - ) - return rgb_to_hex(rgb).lower() - - -def _transform_rgba(value: str, params: dict[str, float]) -> str: - match = _RGBA_RE.match(value) - if match is None: # defensive: non-color metadata handled by caller - return value - r, g, b, alpha = int(match.group(1)), int(match.group(2)), int(match.group(3)), match.group(4) - hue, saturation, lightness = _hsl_from_rgb(r, g, b) - nr, ng, nb = _rgb_from_hsl( - hue, - saturation * params["saturation_factor"], - lightness * params["brightness_factor"], - ) - return f"rgba({nr}, {ng}, {nb}, {alpha})" - - -def _alias_groups(base: dict[str, str]) -> dict[str, list[str]]: - """Map every shared color value to its sorted key list (metadata excluded). - - Members of one group share a color identity (e.g. ``selection == - selection_bg``) and must stay byte-identical after the transform, - preventing independent rounding or correction drift. Incidental equalities - that mix roles (e.g. ``bg == on_accent``) are grouped here but the - corrective pass never propagates onto background/surface roles. - """ - groups: dict[str, list[str]] = {} - for key, value in base.items(): - if key in ("name", "details"): - continue - if not (_HEX_RE.match(value) or _RGBA_RE.match(value)): - continue - groups.setdefault(value.lower(), []).append(key) - return {value: sorted(keys) for value, keys in groups.items() if len(keys) > 1} - - -# Background/surface roles that correction must never touch or brighten: the -# on-accent pair's background is the semantic ``accent`` color, NOT one of -# these roles, so the bounded pass may adjust it to restore the on-accent floor. -_SURFACE_ROLES = frozenset( - { - "bg", - "bg_soft", - "surface0", - "surface1", - "surface2", - "surface3", - "selection", - "selection_bg", - "hover", - "pressed", - "prompt_bg", - "prompt_surface0", - "prompt_surface1", - "prompt_surface2", - } -) - - -def _move_toward_endpoint(out: dict[str, str], token: str, pair_other: str, step: float) -> str: - """Move ``token`` lightness one bounded step toward its pair's contrast - endpoint (dark-on-light darkens, light-on-dark lightens), preserving hue - and the already-reduced saturation.""" - hue, saturation, lightness = _hsl_from_rgb(*hex_to_rgb(out[token])) - if rel_luminance(out[token]) < rel_luminance(out[pair_other]): - lightness = max(0.0, lightness - step) - else: - lightness = min(1.0, lightness + step) - return rgb_to_hex(_rgb_from_hsl(hue, saturation, lightness)).lower() - - -def _apply_with_aliases( - out: dict[str, str], token: str, value: str, members_of: dict[str, list[str]] -) -> None: - """Write a corrected value to the token and its alias-group members, never - onto background/surface roles (which keep their pure-transform value).""" - for member in members_of.get(token, [token]): - if member in _SURFACE_ROLES: - continue - out[member] = value - - -def _corrective_pass( - out: dict[str, str], - guardrails: dict[str, float], - params: dict[str, float], - effective_mode: str, - groups: dict[str, list[str]], -) -> None: - """Bounded lightness correction for failing declared pairs (in place). - - Each step moves a failing foreground's lightness toward its pair's - contrast-safe endpoint (polarity-aware) while preserving hue and the - already-reduced saturation. Total movement per token is structurally - capped at ``maximum_corrective_delta``. Background/surface roles are never - brightened; thresholds are never weakened. The on-accent pair is the one - case whose foreground is pinned at a near-extreme (even pure black on the - accent cannot reach its floor), so its semantic ``accent`` pair-background - -- not a surface role -- receives the bounded adjustment instead; the - corrected value propagates through alias groups so semantic relationships - stay exact (``selection == selection_bg`` etc.). - """ - max_delta = params["maximum_corrective_delta"] - step = params["corrective_step"] - if max_delta <= 0 or step <= 0: - return - text_min = guardrails["minimum_text_contrast"] - # Alias-group membership per token: aliases share one value, so they - # share one corrective budget and step at most once per sweep (otherwise - # e.g. subtle/disabled would double-step the shared value and break the - # maximum_corrective_delta cap). - members_of: dict[str, list[str]] = {} - group_of: dict[str, str] = {} - for keys in groups.values(): - for member in keys: - members_of[member] = keys - group_of[member] = keys[0] - - def rep(token: str) -> str: - return group_of.get(token, token) - - moved: dict[str, float] = {} - max_sweeps = math.ceil(max_delta / step) - for _ in range(max_sweeps): - touched = False - stepped: set[str] = set() - for cls, pairs, light_key, dark_key in _APCA_PAIR_CLASSES: - floor_key = light_key if effective_mode in ("light", "dusk") else dark_key - floor = guardrails[floor_key] - for fg_key, bg_key in pairs: - if fg_key not in out or bg_key not in out: - continue - fg, bg = out[fg_key], out[bg_key] - if abs(apca_lc(fg, bg)) >= floor and contrast(fg, bg) >= text_min: - continue - if bg_key not in _SURFACE_ROLES: - # On-accent: the pair background is the semantic accent - # color (pinned foreground cannot reach the floor); adjust - # the background, bounded, to restore the floor. - target, other = bg_key, fg_key - else: - target, other = fg_key, bg_key - target_rep = rep(target) - if target_rep in stepped: - continue - remaining = max_delta - moved.get(target_rep, 0.0) - if remaining <= 0: - continue - old_lightness = _hsl_from_rgb(*hex_to_rgb(out[target]))[2] - corrected = _move_toward_endpoint(out, target, other, min(step, remaining)) - stepped.add(target_rep) - if corrected == out[target]: - continue # clamped at the endpoint; stop stepping it - new_lightness = _hsl_from_rgb(*hex_to_rgb(corrected))[2] - moved[target_rep] = moved.get(target_rep, 0.0) + abs(new_lightness - old_lightness) - _apply_with_aliases(out, target, corrected, members_of) - touched = True - if not touched: - return - - -def night_palette( - base: dict[str, str], - profile_parameters: dict[str, float], - guardrails: dict[str, float], -) -> dict[str, str]: - """Derive the deterministic Night palette from the dark base (ADR-003). - - Steps (design §2): copy the input (never mutate), set the derived display - name while ``details`` stays ``darker``, reduce HSL lightness/saturation - with deterministic integer-rounded RGB (lowercase hex), preserve ``rgba()`` - alpha exactly, re-establish input aliases, apply the bounded corrective - pass to declared foreground tokens, and reject newly introduced pure - black/white on functional roles. Canonical canvas and on-color roles - explicitly authored as black remain black under Dark's surface policy. - - Parameters and guardrails are canonical (``load_render_profile`` / - ``load_guardrails``); invalid values fail closed with ``ValueError`` and - never fall back to the standard dark palette. - """ - _validate_profile_parameters(profile_parameters) - _require_guardrails(guardrails) - palette_base = {key: value for key, value in base.items() if isinstance(value, str)} - groups = _alias_groups(palette_base) - - out = dict(palette_base) - out["name"] = f"{palette_base.get('name', 'Dreamcoder Dark')} Night" - # details intentionally untouched: the transform keeps dark semantics. - - for key, value in palette_base.items(): - if _HEX_RE.match(value): - out[key] = _transform_hex(value, profile_parameters) - elif _RGBA_RE.match(value): - out[key] = _transform_rgba(value, profile_parameters) - - # Re-establish exact aliases (pure transform keeps equal inputs equal, but - # the corrective pass below must propagate to every member of an alias - # group, so the groups are shared between both stages). - for _value, keys in groups.items(): - first = keys[0] - for member in keys[1:]: - out[member] = out[first] - - effective_mode = detect_mode(out) - _corrective_pass(out, guardrails, profile_parameters, effective_mode, groups) - - for key, value in out.items(): - is_pure = _HEX_RE.match(value) and value.lower() in ("#000000", "#ffffff") - policy_black_roles = {"bg", "prompt_bg", "on_accent", "on_error", "on_focus"} - preserves_authored_extreme = key in policy_black_roles and palette_base.get(key) == value - if is_pure and not preserves_authored_extreme: - raise ValueError(f"night transform produced pure {value} for functional role {key}") - - return out diff --git a/src/dreamcoder_theme/renderer_contract.py b/src/dreamcoder_theme/renderer_contract.py index 89d3aba6..9c2f7f10 100644 --- a/src/dreamcoder_theme/renderer_contract.py +++ b/src/dreamcoder_theme/renderer_contract.py @@ -24,12 +24,12 @@ # leaf renderers and callers compatible; adapters may accept Mapping internally. Palette = dict[str, str] -# Canonical authored themes are distinct from runtime render variants. Night is -# derived from Dark by a render profile; it is never a fourth canonical mode. +# Canonical authored themes are distinct from runtime render variants: Dusk is +# a design-system token set, never a generated runtime variant. CanonicalMode = Literal["light", "dusk", "dark"] CANONICAL_MODES: frozenset[CanonicalMode] = frozenset({"light", "dusk", "dark"}) -RenderVariant = Literal["dark", "light", "night"] +RenderVariant = Literal["dark", "light"] # Backward-compatible registration type name: registrations describe generated # runtime variants, not the canonical token schema. RenderMode = RenderVariant @@ -43,7 +43,7 @@ OutputKind = Literal["active", "repository", "active-and-repository"] # Closed set of generated runtime variants accepted by registrations. -ALL_RENDER_VARIANTS: frozenset[RenderVariant] = frozenset({"dark", "light", "night"}) +ALL_RENDER_VARIANTS: frozenset[RenderVariant] = frozenset({"dark", "light"}) ALL_MODES = ALL_RENDER_VARIANTS diff --git a/src/dreamcoder_theme/renderers_antigravity.py b/src/dreamcoder_theme/renderers_antigravity.py index df31b284..b6f4c51d 100644 --- a/src/dreamcoder_theme/renderers_antigravity.py +++ b/src/dreamcoder_theme/renderers_antigravity.py @@ -6,9 +6,8 @@ def antigravity_content(c: dict[str, str]) -> str: - # Night must be classified dark WITHOUT depending on the word "Dark" in - # the derived name (design §5 row 10): the transformed palette keeps - # dark semantics in ``details``, which is the reliable signal. + # Classify by ``details`` rather than the display name: it is the + # reliable dark/light signal carried by every palette. theme_type = "dark" if c.get("details", "darker") != "lighter" else "light" return json.dumps( { @@ -116,7 +115,7 @@ def antigravity_content(c: dict[str, str]) -> str: consumer_id="antigravity", renderer=antigravity_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_codex.py b/src/dreamcoder_theme/renderers_codex.py index 60305f08..a26662ac 100644 --- a/src/dreamcoder_theme/renderers_codex.py +++ b/src/dreamcoder_theme/renderers_codex.py @@ -67,7 +67,7 @@ def codex_tmtheme_content(c: dict[str, str]) -> str: consumer_id="codex_app", renderer=opencode_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -81,7 +81,7 @@ def codex_tmtheme_content(c: dict[str, str]) -> str: consumer_id="codex_theme", renderer=codex_tmtheme_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -95,7 +95,7 @@ def codex_tmtheme_content(c: dict[str, str]) -> str: consumer_id="bat_theme", renderer=codex_tmtheme_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_bat_delta.py b/src/dreamcoder_theme/renderers_extra_bat_delta.py index 74560147..bf4fdb37 100644 --- a/src/dreamcoder_theme/renderers_extra_bat_delta.py +++ b/src/dreamcoder_theme/renderers_extra_bat_delta.py @@ -8,10 +8,7 @@ def bat_content(c: dict[str, str]) -> str: """Return a Bat theme config snippet with modern defaults.""" mode_name = detect_mode(c) - # The Night snippet selects the Night TextMate sibling (design §5 row - # 17): the derived palette name is the deterministic profile signal - # because the format embeds the theme name. - theme = f"Dreamcoder-{'Night' if 'Night' in c.get('name', '') else mode_name.title()}" + theme = f"Dreamcoder-{mode_name.title()}" return ( "#!/usr/bin/env bash\n" "# Sourced snippet: never set shell options here.\n" @@ -41,11 +38,7 @@ def g(color: str) -> str: minus_bg = mix(c["error"], bg, 0.85) hunk_bg = mix(c["muted"], bg, 0.85) - # Syntax highlighting theme: the Night snippet selects the Night - # TextMate sibling (design §5 row 18) — the format embeds the name. - syntax_theme = ( - "Dreamcoder-Night" if "Night" in c.get("name", "") else f"Dreamcoder-{mode.title()}" - ) + syntax_theme = f"Dreamcoder-{mode.title()}" return f"""# ======================================================== # {c["name"]} — Git Delta theme @@ -110,7 +103,7 @@ def g(color: str) -> str: consumer_id="bat", renderer=bat_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -124,7 +117,7 @@ def g(color: str) -> str: consumer_id="delta", renderer=delta_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_btop.py b/src/dreamcoder_theme/renderers_extra_btop.py index 6a2ed0f7..91eded6e 100644 --- a/src/dreamcoder_theme/renderers_extra_btop.py +++ b/src/dreamcoder_theme/renderers_extra_btop.py @@ -104,7 +104,7 @@ def btop_content(c: dict[str, str]) -> str: consumer_id="btop", renderer=btop_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_firefox.py b/src/dreamcoder_theme/renderers_extra_firefox.py index c7a78240..cbf524e5 100644 --- a/src/dreamcoder_theme/renderers_extra_firefox.py +++ b/src/dreamcoder_theme/renderers_extra_firefox.py @@ -180,7 +180,7 @@ def firefox_content(c: dict[str, str]) -> str: consumer_id="firefox", renderer=firefox_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_notify.py b/src/dreamcoder_theme/renderers_extra_notify.py index 58bb46df..02f8f828 100644 --- a/src/dreamcoder_theme/renderers_extra_notify.py +++ b/src/dreamcoder_theme/renderers_extra_notify.py @@ -94,7 +94,7 @@ def cava_content(c: dict[str, str]) -> str: consumer_id="dunst", renderer=dunst_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -108,7 +108,7 @@ def cava_content(c: dict[str, str]) -> str: consumer_id="cava", renderer=cava_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_nvim.py b/src/dreamcoder_theme/renderers_extra_nvim.py index 40f01dd4..21232ba2 100644 --- a/src/dreamcoder_theme/renderers_extra_nvim.py +++ b/src/dreamcoder_theme/renderers_extra_nvim.py @@ -14,23 +14,21 @@ def nvim_dispatcher_content() -> str: """Return a dispatcher that loads the correct variant. Priority: - 1. DREAMCODER_THEME_PROFILE env var (night) — resolved BEFORE base mode - 2. DREAMCODER_THEME_MODE env var (set by `dreamcoder dark` / `dreamcoder light`) - 3. ~/.cache/dreamcoder/cursor-cli.env (written by apply-theme-mode.sh) - 4. vim.o.background (Neovim's own mode) - 5. Fallback: "dark" + 1. DREAMCODER_THEME_MODE env var (set by `dreamcoder dark` / `dreamcoder light`) + 2. ~/.cache/dreamcoder/cursor-cli.env (written by apply-theme-mode.sh) + 3. vim.o.background (Neovim's own mode) + 4. Fallback: "dark" """ return """-- ======================================================== -- Dreamcoder — auto-detect dispatcher -- ======================================================== -- Usage: vim.cmd.colorscheme("dreamcoder") -- Auto-loads the correct variant based on: --- 1. DREAMCODER_THEME_PROFILE env var (night) — before base mode --- 2. DREAMCODER_THEME_MODE env var (set by `dreamcoder light`) --- 3. ~/.cache/dreamcoder/cursor-cli.env (persisted by apply-theme-mode.sh) --- 4. vim.o.background (Neovim's own setting) --- 5. Fallback: dark --- Direct variant access: colorscheme dreamcoder-dark / dreamcoder-light / dreamcoder-night +-- 1. DREAMCODER_THEME_MODE env var (set by `dreamcoder light`) +-- 2. ~/.cache/dreamcoder/cursor-cli.env (persisted by apply-theme-mode.sh) +-- 3. vim.o.background (Neovim's own setting) +-- 4. Fallback: dark +-- Direct variant access: colorscheme dreamcoder-dark / dreamcoder-light -- ======================================================== vim.g.colors_name = "dreamcoder" @@ -43,29 +41,6 @@ def nvim_dispatcher_content() -> str: local src = debug.getinfo(1, "S").source:match("@?(.*)") local theme_dir = src:match("^(.*[/\\\\])") or "." --- Resolve render profile first (night wins over base mode), then mode. -local profile = vim.env.DREAMCODER_THEME_PROFILE - -if not profile then - local cache_file = vim.fn.expand("~/.cache/dreamcoder/cursor-cli.env") - local f = io.open(cache_file, "r") - if f then - for line in f:lines() do - local m = line:match('^export DREAMCODER_THEME_PROFILE="(.-)"') - if m then - profile = m - break - end - end - f:close() - end -end - -if profile == "night" then - dofile(theme_dir .. "dreamcoder-night.lua") - return -end - -- Resolve base mode: env var > cache file > vim.o.background > dark local mode = vim.env.DREAMCODER_THEME_MODE @@ -123,10 +98,7 @@ def hl(name: str, **kwargs: Any) -> str: return f' vim.api.nvim_set_hl(0, "{name}", {{\n{inner}\n }})\n' invert = c["details"] == "lighter" - # The derived Night variant reports its own identity (design §5 row 14) - # so a direct ``colorscheme dreamcoder-night`` stays correct; standard - # dark/light keep today's names. - variant_name = "night" if "Night" in c.get("name", "") else ("light" if invert else "dark") + variant_name = "light" if invert else "dark" bg = c["bg"] normal_bg = bg if invert else "none" text = c["text"] @@ -199,7 +171,7 @@ def hl(name: str, **kwargs: Any) -> str: consumer_id="nvim", renderer=NvimDispatcherAdapter(), contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.NVIM_DISPATCHER, diff --git a/src/dreamcoder_theme/renderers_extra_obsidian.py b/src/dreamcoder_theme/renderers_extra_obsidian.py index baf45e1f..ce7b5e81 100644 --- a/src/dreamcoder_theme/renderers_extra_obsidian.py +++ b/src/dreamcoder_theme/renderers_extra_obsidian.py @@ -174,7 +174,7 @@ def obsidian_content(c: dict[str, str]) -> str: consumer_id="obsidian", renderer=obsidian_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_extra_shell.py b/src/dreamcoder_theme/renderers_extra_shell.py index 1b2dc6ab..b83a0176 100644 --- a/src/dreamcoder_theme/renderers_extra_shell.py +++ b/src/dreamcoder_theme/renderers_extra_shell.py @@ -236,7 +236,7 @@ def fzf_content(c: dict[str, str]) -> str: consumer_id="zsh_syntax", renderer=zsh_syntax_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -250,7 +250,7 @@ def fzf_content(c: dict[str, str]) -> str: consumer_id="ls_colors", renderer=ls_colors_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -264,7 +264,7 @@ def fzf_content(c: dict[str, str]) -> str: consumer_id="fzf", renderer=fzf_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_ghostty_warp.py b/src/dreamcoder_theme/renderers_ghostty_warp.py index a16cf575..013599e4 100644 --- a/src/dreamcoder_theme/renderers_ghostty_warp.py +++ b/src/dreamcoder_theme/renderers_ghostty_warp.py @@ -73,7 +73,7 @@ def warp_content(c: dict[str, str]) -> str: consumer_id="ghostty", renderer=ghostty_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -87,7 +87,7 @@ def warp_content(c: dict[str, str]) -> str: consumer_id="warp", renderer=warp_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_herdr.py b/src/dreamcoder_theme/renderers_herdr.py index 708d725a..8786e7ad 100644 --- a/src/dreamcoder_theme/renderers_herdr.py +++ b/src/dreamcoder_theme/renderers_herdr.py @@ -49,11 +49,11 @@ class HerdrModeError(Exception): def herdr_content(profile: HerdrProfile, mode: str, palette: dict[str, str]) -> str: - """Render one static Light, Dark, or Night variant without touching active configuration.""" + """Render one static Light or Dark variant without touching active configuration.""" if not profile.is_complete: raise HerdrContractUnavailableError("Herdr color rendering requires a complete profile") - if mode not in {"dark", "light", "night"}: - raise HerdrModeError("Herdr supports only dark, light, and night variants") + if mode not in {"dark", "light"}: + raise HerdrModeError("Herdr supports only dark and light variants") evidence = profile.evidence if "name" not in evidence.allowed_theme_fields: @@ -146,7 +146,7 @@ def herdr_ui_field_rhs() -> dict[str, str]: consumer_id="herdr", renderer=VersionedHerdrAdapter(_REPRESENTATIVE_PROFILE, "dark"), contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="repository", sync=SyncDefinition( renderer=RendererStrategy.VERSIONED_HERDR, diff --git a/src/dreamcoder_theme/renderers_hypr_waybar_rofi.py b/src/dreamcoder_theme/renderers_hypr_waybar_rofi.py index 0b6257e8..9baa6afe 100644 --- a/src/dreamcoder_theme/renderers_hypr_waybar_rofi.py +++ b/src/dreamcoder_theme/renderers_hypr_waybar_rofi.py @@ -360,7 +360,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="hyprland", renderer=hypr_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -374,7 +374,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="hypr_colors_lua", renderer=hypr_colors_lua_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), # Verified against live sync.py (not the prior "active"/NO_VARIANTS/ # ACTIVE_ONLY_BRIDGE declaration): sync_active_targets() writes the # live ~/.config/hypr/colors.lua active file via plain @@ -397,7 +397,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="hypr_colors_conf", renderer=hypr_colors_conf_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), # See hypr_colors_lua comment above — identical verified shape: # live active write (sync_active_targets -> paths.hypr_colors_conf) # plus repo MODE_VARIANTS snippets (sync_repo_snippets), both via @@ -415,7 +415,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="waybar", renderer=waybar_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -429,7 +429,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="waybar_matugen", renderer=waybar_matugen_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -443,7 +443,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="rofi", renderer=rofi_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -457,7 +457,7 @@ def _hex_to_rgb_int(hex_color: str) -> tuple[int, int, int]: consumer_id="rofi_matugen", renderer=rofi_matugen_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_kitty.py b/src/dreamcoder_theme/renderers_kitty.py index 045d4526..6dbd2c68 100644 --- a/src/dreamcoder_theme/renderers_kitty.py +++ b/src/dreamcoder_theme/renderers_kitty.py @@ -139,7 +139,7 @@ def kitty_ui_content(c: dict[str, str]) -> str: consumer_id="kitty", renderer=kitty_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, @@ -153,7 +153,7 @@ def kitty_ui_content(c: dict[str, str]) -> str: consumer_id="kitty_ui", renderer=kitty_ui_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_lazygit.py b/src/dreamcoder_theme/renderers_lazygit.py index c1aecf44..72fd83ba 100644 --- a/src/dreamcoder_theme/renderers_lazygit.py +++ b/src/dreamcoder_theme/renderers_lazygit.py @@ -1,6 +1,6 @@ """Lazygit theme renderer for Dreamcoder. -Generates full ``config.yml`` files (active + ``config.{dark,light,night}.yml`` +Generates full ``config.yml`` files (active + ``config.{dark,light}.yml`` variants) from the canonical palette tokens. Non-color behavior is preserved verbatim from the previous static config; only colors are derived from tokens, so there is no duplicated per-mode hardcoded palette anywhere. @@ -16,8 +16,7 @@ def _delta_syntax_theme(c: dict[str, str]) -> str: The Dreamcoder ``*-light`` TextMate theme does not exist, so Lazygit's embedded Delta renderer must use installed valid Catppuccin themes: - Latte for light, Mocha for dark and the Night derivation (ADR-003 keeps - Night dark semantics, so ``details`` stays ``darker``). + Latte for light and Mocha for dark. """ return "Catppuccin Latte" if detect_mode(c) == "light" else "Catppuccin Mocha" @@ -118,7 +117,7 @@ def lazygit_content(c: dict[str, str]) -> str: consumer_id="lazygit", renderer=lazygit_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_opencode.py b/src/dreamcoder_theme/renderers_opencode.py index f7b8c67b..5412534e 100644 --- a/src/dreamcoder_theme/renderers_opencode.py +++ b/src/dreamcoder_theme/renderers_opencode.py @@ -232,7 +232,7 @@ def opencode_content(c: dict[str, str], transparent_background: bool = False) -> consumer_id="opencode", renderer=TransparentOpenCodeAdapter(), contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active", sync=SyncDefinition( renderer=RendererStrategy.TRANSPARENT_OPENCODE, diff --git a/src/dreamcoder_theme/renderers_pi.py b/src/dreamcoder_theme/renderers_pi.py index 57a86eea..a519debb 100644 --- a/src/dreamcoder_theme/renderers_pi.py +++ b/src/dreamcoder_theme/renderers_pi.py @@ -104,7 +104,7 @@ def pi_theme_content(c: dict[str, str]) -> str: consumer_id="pi_theme", renderer=pi_theme_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_readme.py b/src/dreamcoder_theme/renderers_readme.py index 6283347d..753712ba 100644 --- a/src/dreamcoder_theme/renderers_readme.py +++ b/src/dreamcoder_theme/renderers_readme.py @@ -12,7 +12,6 @@ def readme_content() -> str: - `tokens.schema.json`: machine-readable token contract. - `*-dark.*`: Dreamcoder Dark mode with near-black bg, cool steel-blue accents, and muted slate surfaces. - `*-light.*`: paper-like daytime mode with flat surface ladder and distinct semantic tokens. -- `*-night.*`: Night/Dim rendering profile — deterministic brightness/saturation-reduced derivation of Dreamcoder Dark (user-controlled display profile, no medical claims). ## Color-only Snippets (Hooks) Hook these into your app configs after ML4W/Gentleman files: diff --git a/src/dreamcoder_theme/renderers_starship.py b/src/dreamcoder_theme/renderers_starship.py index 0a94cdfb..619d8bd0 100644 --- a/src/dreamcoder_theme/renderers_starship.py +++ b/src/dreamcoder_theme/renderers_starship.py @@ -21,11 +21,7 @@ def starship_content(c: dict[str, str]) -> str: diag = c["diagnostic"] lavender_col = c["lavender"] mauve_col = c["mauve"] - # The named Night sibling must never reference a standard-dark palette - # section (design §5 row 11): the derived name is the deterministic - # profile signal because the format embeds the palette identity. - is_night = "Night" in c.get("name", "") - palette_id = "dreamcoder-night" if is_night else "dreamcoder" + palette_id = "dreamcoder" return f'''# ======================================================== # {c["name"]} — Starship prompt @@ -191,7 +187,7 @@ def starship_content(c: dict[str, str]) -> str: consumer_id="starship", renderer=starship_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_tmux.py b/src/dreamcoder_theme/renderers_tmux.py index b97feb6e..80afb37b 100644 --- a/src/dreamcoder_theme/renderers_tmux.py +++ b/src/dreamcoder_theme/renderers_tmux.py @@ -93,7 +93,7 @@ def tmux_content(c: dict[str, str]) -> str: consumer_id="tmux", renderer=tmux_content, contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.DIRECT_CONTENT, diff --git a/src/dreamcoder_theme/renderers_zellij.py b/src/dreamcoder_theme/renderers_zellij.py index 52a031e6..2d8c50f7 100644 --- a/src/dreamcoder_theme/renderers_zellij.py +++ b/src/dreamcoder_theme/renderers_zellij.py @@ -13,8 +13,8 @@ def zellij_content(c: dict[str, str], theme_name: str) -> str: """Render a Zellij KDL theme block from a palette dictionary. ``theme_name`` is the named theme key emitted inside ``themes { }`` (for - example ``dreamcoder-night``); colors are derived from the (transformed) - palette, never hand-tuned (ADR-003/ADR-004). + example ``dreamcoder-dark``); colors are derived from the palette, never + hand-tuned (ADR-004). """ return f"""// {c.get("name", "Dreamcoder")} — Zellij theme ({theme_name}) // Auto-generated from tokens.json — do not edit manually @@ -73,7 +73,7 @@ def zellij_content(c: dict[str, str], theme_name: str) -> str: consumer_id="zellij", renderer=NamedZellijAdapter("dreamcoder"), contract_version=1, - modes=frozenset({"dark", "light", "night"}), + modes=frozenset({"dark", "light"}), output_kind="active-and-repository", sync=SyncDefinition( renderer=RendererStrategy.NAMED_ZELLIJ, diff --git a/src/dreamcoder_theme/repair_engine.py b/src/dreamcoder_theme/repair_engine.py index a777c98b..a2bcb91d 100644 --- a/src/dreamcoder_theme/repair_engine.py +++ b/src/dreamcoder_theme/repair_engine.py @@ -107,10 +107,10 @@ def repair_plan() -> dict[str, Any]: "command": "./scripts/dreamcoder installer plan --json", } ) - elif check.name == "day/night timer": + elif check.name == "light/dark timer": actions.append( { - "id": "enable-day-night-timer", + "id": "enable-light-dark-timer", "check": check.name, "safe": False, "target": "dreamcoder-theme-auto.timer", diff --git a/src/dreamcoder_theme/settings.py b/src/dreamcoder_theme/settings.py index fd2c7545..3814f4fd 100644 --- a/src/dreamcoder_theme/settings.py +++ b/src/dreamcoder_theme/settings.py @@ -7,8 +7,6 @@ from dataclasses import dataclass, field from pathlib import Path -from .settings_store import settings_get - ROOT = Path(__file__).resolve().parents[2] PI_THEME_SCHEMA = ( "https://raw.githubusercontent.com/earendil-works/pi/main/" @@ -108,61 +106,6 @@ def theme_mode() -> str: return mode -VALID_RENDER_PROFILES = frozenset({"standard", "night"}) - - -def render_profile() -> str: - """Resolve the effective rendering profile (design §3, R6). - - Precedence: - 1. ``DREAMCODER_THEME_PROFILE`` — process-only override for isolated - generation and tests; it never mutates the persisted setting. - 2. Persisted ``settings_get("theme.render_profile")``. - 3. Schema default ``standard`` when absent. - - Invalid values fail closed instead of being interpreted as a runtime - profile (R6 scenario "Invalid profile value is rejected"). - """ - env_profile = os.environ.get("DREAMCODER_THEME_PROFILE") - if env_profile is not None: - profile = env_profile.lower() - if profile not in VALID_RENDER_PROFILES: - raise SystemExit( - f"DREAMCODER_THEME_PROFILE must be 'standard' or 'night' (got '{env_profile}')" - ) - return profile - persisted = settings_get("theme.render_profile") - if persisted is not None: - if not isinstance(persisted, str) or persisted not in VALID_RENDER_PROFILES: - raise SystemExit( - f"persisted theme.render_profile={persisted!r} is invalid; " - "expected 'standard' or 'night'" - ) - return persisted - return "standard" - - -def effective_base_mode(mode: str | None = None, profile: str | None = None) -> str: - """Resolve the base mode together with the render profile (ADR-003). - - Enforces ``profile == night -> mode == dark``: Night always derives from - the Dreamcoder Dark base. A conflicting invocation such as - ``DREAMCODER_THEME_MODE=light`` + ``DREAMCODER_THEME_PROFILE=night`` fails - with an actionable error instead of choosing Dusk or silently coercing - output (design §3). ``theme_mode()`` remains the Light/Dark base authority - and never accepts Night as a base mode. - """ - base = theme_mode() if mode is None else mode - resolved_profile = render_profile() if profile is None else profile - if resolved_profile == "night" and base != "dark": - raise SystemExit( - f"render profile 'night' requires base mode 'dark' (got '{base}'): " - "Night always derives from the Dreamcoder Dark base; " - "set DREAMCODER_THEME_MODE=dark or run the 'night' activation." - ) - return base - - def theme_paths() -> ThemePaths: config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")) diff --git a/src/dreamcoder_theme/settings_store.py b/src/dreamcoder_theme/settings_store.py index 2c418c2f..166ae294 100644 --- a/src/dreamcoder_theme/settings_store.py +++ b/src/dreamcoder_theme/settings_store.py @@ -24,12 +24,6 @@ "default": "balanced", "description": "Active motion preset.", }, - "theme.render_profile": { - "type": "string", - "enum": ["standard", "night"], - "default": "standard", - "description": "Theme rendering profile (standard or night).", - }, } diff --git a/src/dreamcoder_theme/sync.py b/src/dreamcoder_theme/sync.py index 69e9031c..170d9958 100644 --- a/src/dreamcoder_theme/sync.py +++ b/src/dreamcoder_theme/sync.py @@ -13,9 +13,7 @@ from .palette import ( adaptive_palette, load_guardrails, - load_render_profile, load_variants, - night_palette, validate_palette, ) from .palette_tokens import VARIANTS as DEFAULT_VARIANTS @@ -57,9 +55,7 @@ from .renderers_orca import sync_orca_theme from .settings import ( ROOT, - VALID_RENDER_PROFILES, adaptive_enabled, - render_profile, theme_mode, theme_paths, write_repo_enabled, @@ -82,7 +78,7 @@ class ThemeGateError(RuntimeError): """Raised by ``prepare()`` when the dual gate or coverage assertion fails. - Carries the structured failure list (metric/profile/pair/measured/ + Carries the structured failure list (metric/mode/pair/measured/ threshold for WCAG+APCA, or the coverage problems) and renders the fail-closed message main() and the CLI handler surface. It is raised before any writer, selector, or settings mutation (R4/R8, design §4/§8). @@ -100,31 +96,28 @@ class PreparedSync: """Immutable result of validation-first preparation (design §4, ADR-004). ``prepare()`` produces this with ZERO filesystem writes: the validated - active palette, the dark/light/night render-variant map, the frozen + active palette, the dark/light render-variant map, the frozen 33-consumer coverage declaration, and the in-memory render of every coverage consumer. The caller (``main()`` or the CLI activation transaction) owns the commit. """ mode: str - profile: str active: dict[str, str] variants: dict[str, dict[str, str]] coverage: tuple[CoverageRow, ...] render_plan: Mapping[str, str] -def sync_active_targets( - paths: Any, active: dict[str, str], mode: str, profile: str = "standard" -) -> dict[str, bool]: +def sync_active_targets(paths: Any, active: dict[str, str], mode: str) -> dict[str, bool]: return { "kitty": write_if_changed(paths.kitty, kitty_content(active)), "kitty_ui": write_if_changed(paths.kitty_ui, kitty_ui_content(active)), "kitty_config": ensure_kitty_ui_include(paths.kitty_config), "ghostty": write_if_changed(paths.ghostty, ghostty_content(active)), - "ghostty_config": update_ghostty_theme(paths.ghostty_config, mode, profile), + "ghostty_config": update_ghostty_theme(paths.ghostty_config, mode), "warp": write_if_changed(paths.warp, warp_content(active)), - "warp_settings": update_warp_settings(paths.warp_settings, mode, profile), + "warp_settings": update_warp_settings(paths.warp_settings, mode), "opencode": write_if_changed( paths.opencode, opencode_content(active, transparent_background=True) ), @@ -140,7 +133,7 @@ def sync_active_targets( "starship": write_if_changed(paths.starship, starship_content(active)), "tmux": write_if_changed(paths.tmux, tmux_content(active)), "lazygit": write_if_changed(paths.lazygit, lazygit_content(active)), - "zellij": update_zellij_config(paths.zellij_config, mode, profile), + "zellij": update_zellij_config(paths.zellij_config, mode), # New targets "nvim": write_if_changed(paths.nvim, nvim_dispatcher_content()), "zsh_syntax": write_if_changed(paths.zsh_syntax, zsh_syntax_content(active)), @@ -172,26 +165,27 @@ def sync_active_targets( # hyprland, waybar, rofi, nvim, and opencode-transparent # stay as explicit calls below the loop. # ------------------------------------------------------------------ -D = {"dark": "dark", "light": "light", "night": "night"} +D = {"dark": "dark", "light": "light"} # ------------------------------------------------------------------ -# Exact 33-consumer Night coverage declaration (design §5 matrix). +# Exact 33-consumer coverage declaration (design §5 matrix). # ------------------------------------------------------------------ -# ``night_artifact`` is the deterministic Night output path (relative to -# ROOT, POSIX separators) for repo-generation rows. Active-only matugen -# bridges carry an ``active:`` marker because their Night delivery is -# the live ``colors.css``/``colors.rasi`` file, not a repository artifact. -# ``source`` records which sync branch owns the row (registry loop, explicit +# ``artifact`` is the deterministic Dark output path (relative to ROOT, POSIX +# separators) for repo-generation rows; the Light sibling follows the same +# naming with ``light``/``Light``. Active-only matugen bridges carry an +# ``active:`` marker because their delivery is the live +# ``colors.css``/``colors.rasi`` file, not a repository artifact. ``source`` +# records which sync branch owns the row (registry loop, explicit # sync_repo_snippets() branch, or Herdr) and is the bijection-test hook. class CoverageRow(NamedTuple): - """One row of the 33-consumer Night coverage contract (design §5).""" + """One row of the 33-consumer coverage contract (design §5).""" consumer_id: str klass: str writer: str - night_artifact: str + artifact: str selection_strategy: str source: str @@ -201,176 +195,176 @@ class CoverageRow(NamedTuple): "kitty", "variant file + active-selected", "write_variant_files + write_if_changed; kitty_content", - "DreamcoderKitty/.config/kitty/colors-dreamcoder-night.conf", - "active symlink/file selects or receives Night", + "DreamcoderKitty/.config/kitty/colors-dreamcoder-dark.conf", + "active symlink/file selects or receives the mode", "registry", ), CoverageRow( "kitty_ui", "variant file + active-selected", "write_variant_files + write_if_changed; kitty_ui_content", - "DreamcoderKitty/.config/kitty/dreamcoder-ui-night.conf", - "stable dreamcoder-ui.conf includes/contains Night", + "DreamcoderKitty/.config/kitty/dreamcoder-ui-dark.conf", + "stable dreamcoder-ui.conf includes/contains the mode", "registry", ), CoverageRow( "ghostty", "variant file + active-selected", "write_variant_files + update_ghostty_theme; ghostty_content", - "DreamcoderGhostty/.config/ghostty/themes/dreamcoder-night", - "select theme = dreamcoder-night", + "DreamcoderGhostty/.config/ghostty/themes/dreamcoder-dark", + "select theme = dreamcoder-dark / dreamcoder", "registry", ), CoverageRow( "warp", "variant file + active-selected", "write_variant_files + update_warp_settings; warp_content", - "DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Night.yaml", - "active symlink/file selects it; dark opacity/blur semantics", + "DreamcoderWarp/.local/share/warp-terminal/themes/Dreamcoder-Dark.yaml", + "active symlink/file selects it; mode-aware opacity/blur", "registry", ), CoverageRow( "starship", "variant file + active-selected", "write_variant_files + write_if_changed; starship_content", - "DreamcoderShell/.config/starship-night.toml", - "palette section never standard-dark ([palettes.dreamcoder-night])", + "DreamcoderShell/.config/starship-dark.toml", + "palette section [palettes.dreamcoder]", "registry", ), CoverageRow( "codex_app", "variant file + active-selected", "write_variant_files + write_if_changed; opencode_content", - "DreamcoderCodexApp/Dreamcoder-Night.codex-theme.json", - "stable Dreamcoder.codex-theme.json receives Night", + "DreamcoderCodexApp/Dreamcoder-Dark.codex-theme.json", + "stable Dreamcoder.codex-theme.json receives the active mode", "registry", ), CoverageRow( "codex_theme", "variant file + active-selected", "write_variant_files + write_if_changed; codex_tmtheme_content", - "DreamcoderCodexCLI/Dreamcoder-Night.tmTheme", - "stable Dreamcoder.tmTheme receives Night", + "DreamcoderCodexCLI/Dreamcoder-Dark.tmTheme", + "stable Dreamcoder.tmTheme receives the active mode", "registry", ), CoverageRow( "bat_theme", "variant file + active-selected", "write_variant_files + write_if_changed; codex_tmtheme_content", - "DreamcoderBat/.config/bat/themes/Dreamcoder-Night.tmTheme", - "stable Dreamcoder.tmTheme receives Night", + "DreamcoderBat/.config/bat/themes/Dreamcoder-Dark.tmTheme", + "stable Dreamcoder.tmTheme receives the active mode", "registry", ), CoverageRow( "pi_theme", "variant file + active-selected", "write_variant_files + write_if_changed; pi_theme_content", - "DreamcoderPi/.pi/agent/themes/dreamcoder-night.json", - "stable dreamcoder.json receives/selects Night", + "DreamcoderPi/.pi/agent/themes/dreamcoder-dark.json", + "stable dreamcoder.json receives/selects the active mode", "registry", ), CoverageRow( "antigravity", "variant file + active-selected", "write_variant_files + write_if_changed; antigravity_content", - "DreamcoderAntigravity/Dreamcoder-Night.json", - "stable Dreamcoder.json receives Night; classified dark without name detection", + "DreamcoderAntigravity/Dreamcoder-Dark.json", + "stable Dreamcoder.json pinned to Dark; classified by details", "registry", ), CoverageRow( "tmux", "variant file + active-selected", "write_variant_files + write_if_changed; tmux_content", - "DreamcoderThemes/dreamcoder/tmux-dreamcoder-night.conf", - "active file receives Night", + "DreamcoderThemes/dreamcoder/tmux-dreamcoder-dark.conf", + "active file receives the mode", "registry", ), CoverageRow( "lazygit", "variant file + active-selected", "write_variant_files + write_if_changed; lazygit_content", - "DreamcoderLazygit/.config/lazygit/config.night.yml", - "live ~/.config/lazygit/config.yml symlink selects Night", + "DreamcoderLazygit/.config/lazygit/config.dark.yml", + "live ~/.config/lazygit/config.yml symlink selects the mode", "registry", ), CoverageRow( "zsh_syntax", "snippet + active-selected", "write_variant_files + write_if_changed; zsh_syntax_content", - "DreamcoderThemes/dreamcoder/zsh-syntax-highlighting-dreamcoder-night.zsh", - "active sourced file receives/selects Night", + "DreamcoderThemes/dreamcoder/zsh-syntax-highlighting-dreamcoder-dark.zsh", + "active sourced file receives/selects the mode", "registry", ), CoverageRow( "ls_colors", "snippet + active-selected", "write_variant_files + write_if_changed; ls_colors_content", - "DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh", - "active sourced file receives/selects Night", + "DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-dark.sh", + "active sourced file receives/selects the mode", "registry", ), CoverageRow( "bat", "snippet + active-selected", "write_variant_files + write_if_changed; bat_content", - "DreamcoderThemes/dreamcoder/bat-dreamcoder-night.sh", - "selects the Night TextMate sibling (BAT_THEME=Dreamcoder-Night)", + "DreamcoderThemes/dreamcoder/bat-dreamcoder-dark.sh", + "selects the mode TextMate sibling (BAT_THEME=Dreamcoder-Dark)", "registry", ), CoverageRow( "delta", "snippet + active-selected", "write_variant_files + write_if_changed; delta_content", - "DreamcoderThemes/dreamcoder/delta-dreamcoder-night.gitconfig", - "active include/symlink selects Night; syntax-theme=Dreamcoder-Night", + "DreamcoderThemes/dreamcoder/delta-dreamcoder-dark.gitconfig", + "active include/symlink selects the mode; syntax-theme=Dreamcoder-Dark", "registry", ), CoverageRow( "fzf", "snippet + active-selected", "write_variant_files + write_if_changed; fzf_content", - "DreamcoderThemes/dreamcoder/fzf-dreamcoder-night.sh", - "active sourced file receives/selects Night", + "DreamcoderThemes/dreamcoder/fzf-dreamcoder-dark.sh", + "active sourced file receives/selects the mode", "registry", ), CoverageRow( "btop", "variant file + active-selected", "write_variant_files + write_if_changed; btop_content", - "DreamcoderThemes/dreamcoder/btop-dreamcoder-night.theme", - "active dreamcoder.theme symlink selects Night", + "DreamcoderThemes/dreamcoder/btop-dreamcoder-dark.theme", + "active dreamcoder.theme symlink selects the mode", "registry", ), CoverageRow( "dunst", "snippet + active-selected", "write_variant_files + write_if_changed; dunst_content", - "DreamcoderThemes/dreamcoder/dunst-dreamcoder-night.conf", - "active included file receives/selects Night", + "DreamcoderThemes/dreamcoder/dunst-dreamcoder-dark.conf", + "active included file receives/selects the mode", "registry", ), CoverageRow( "firefox", "snippet + active-selected", "write_variant_files + write_if_changed; firefox_content", - "DreamcoderThemes/dreamcoder/firefox-dreamcoder-night.css", - "active userChrome import receives/selects Night", + "DreamcoderThemes/dreamcoder/firefox-dreamcoder-dark.css", + "active userChrome import receives/selects the mode", "registry", ), CoverageRow( "obsidian", "snippet + active-selected", "write_variant_files + write_if_changed; obsidian_content", - "DreamcoderThemes/dreamcoder/obsidian-dreamcoder-night.css", - "active snippet receives/selects Night and keeps .theme-dark", + "DreamcoderThemes/dreamcoder/obsidian-dreamcoder-dark.css", + "active snippet receives/selects the mode", "registry", ), CoverageRow( "cava", "snippet + active-selected", "write_variant_files + write_if_changed; cava_content", - "DreamcoderThemes/dreamcoder/cava-dreamcoder-night.config", - "active include receives/selects Night", + "DreamcoderThemes/dreamcoder/cava-dreamcoder-dark.config", + "active include receives/selects the mode", "registry", ), CoverageRow( @@ -378,55 +372,55 @@ class CoverageRow(NamedTuple): "active-selected", "write_if_changed; opencode_content(transparent_background=True)", ".opencode/themes/dreamcoder.json", - "stable theme ID dreamcoder overwritten with Night; no dreamcoder-night.json sibling", + "stable theme ID dreamcoder overwritten with the active mode", "explicit", ), CoverageRow( "zellij", "variant file + active-selected", "write_if_changed; zellij_content + update_zellij_config", - "DreamcoderZellij/.config/zellij/dreamcoder-night.kdl", - 'generate KDL with themes { dreamcoder-night } and select theme "dreamcoder-night"', + "DreamcoderZellij/.config/zellij/dreamcoder-dark.kdl", + 'generate KDL with themes { dreamcoder-dark } and select theme "dreamcoder-dark"', "explicit", ), CoverageRow( "nvim", "variant file + active-selected", "write_variant_files + nvim_dispatcher_content; nvim_content", - "DreamcoderNvim/.config/nvim/colors/dreamcoder-night.lua", - "dispatcher resolves DREAMCODER_THEME_PROFILE before base mode", + "DreamcoderNvim/.config/nvim/colors/dreamcoder-dark.lua", + "dispatcher resolves DREAMCODER_THEME_MODE", "explicit", ), CoverageRow( "hyprland", "variant file + active-selected", "write_if_changed; hypr_content", - "DreamcoderThemes/dreamcoder/hyprland-night.conf", - "stable hyprland.conf receives Night; shell selector may point to the Night sibling", + "DreamcoderThemes/dreamcoder/hyprland-dark.conf", + "stable hyprland.conf receives the mode; shell selector may point to a sibling", "explicit", ), CoverageRow( "hypr_colors_lua", "snippet + active-selected", "write_variant_files; hypr_colors_lua_content", - "DreamcoderThemes/dreamcoder/hypr-colors-night.lua", - "active symlink/file selects Night", + "DreamcoderThemes/dreamcoder/hypr-colors-dark.lua", + "active symlink/file selects the mode", "explicit", ), CoverageRow( "hypr_colors_conf", "snippet + active-selected", "write_variant_files; hypr_colors_conf_content", - "DreamcoderThemes/dreamcoder/hypr-colors-night.conf", - "active symlink/file selects Night", + "DreamcoderThemes/dreamcoder/hypr-colors-dark.conf", + "active symlink/file selects the mode", "explicit", ), CoverageRow( "waybar", "variant file + active-selected", "write_if_changed; waybar_content", - "DreamcoderThemes/dreamcoder/waybar-night.css", - "stable/selected Waybar CSS receives Night", + "DreamcoderThemes/dreamcoder/waybar-dark.css", + "stable/selected Waybar CSS receives the mode", "explicit", ), CoverageRow( @@ -434,15 +428,15 @@ class CoverageRow(NamedTuple): "snippet + active-selected", "write_if_changed; waybar_matugen_content", "active:waybar/colors.css", - "write transformed Night directly; symlink-aware colors-night.css selection", + "write the mode directly; symlink-aware colors-{mode}.css selection", "explicit", ), CoverageRow( "rofi", "variant file + active-selected", "write_if_changed; rofi_content", - "DreamcoderThemes/dreamcoder/rofi-night.rasi", - "stable/selected Rofi theme receives Night", + "DreamcoderThemes/dreamcoder/rofi-dark.rasi", + "stable/selected Rofi theme receives the mode", "explicit", ), CoverageRow( @@ -450,22 +444,22 @@ class CoverageRow(NamedTuple): "snippet + active-selected", "write_if_changed; rofi_matugen_content", "active:rofi/colors.rasi", - "write transformed Night directly; symlink-aware colors-night.rasi selection", + "write the mode directly; symlink-aware colors-{mode}.rasi selection", "explicit", ), CoverageRow( "herdr", "variant file", "sync_herdr_repo_variants + write_if_changed; herdr_content", - "DreamcoderHerdr/.config/herdr/dreamcoder//config.night.toml", - "config.night.toml for every complete SUPPORTED_PROFILES entry; repository-only, no live activation", + "DreamcoderHerdr/.config/herdr/dreamcoder//config.dark.toml", + "config.{dark,light}.toml for every complete SUPPORTED_PROFILES entry; repository-only", "herdr", ), ) def validate_coverage_declaration(rows: tuple[CoverageRow, ...] = COVERAGE) -> list[str]: - """Fail closed on missing, duplicate, or undeclared Night coverage. + """Fail closed on missing, duplicate, or undeclared consumer coverage. Internal consistency gate (R5): the registry loop and the explicit branches must each be represented by exactly one row, with no duplicate @@ -481,13 +475,13 @@ def validate_coverage_declaration(rows: tuple[CoverageRow, ...] = COVERAGE) -> l if len(rows) != 33: problems.append(f"coverage declares {len(rows)} rows, expected exactly 33") - registry_night = { - (base / names["night"]).relative_to(ROOT).as_posix() + registry_dark = { + (base / names["dark"]).relative_to(ROOT).as_posix() for base, names, _builder, _active in VARIANT_REGISTRY } - declared_registry = {row.night_artifact for row in rows if row.source == "registry"} - missing = sorted(registry_night - declared_registry) - extra = sorted(declared_registry - registry_night) + declared_registry = {row.artifact for row in rows if row.source == "registry"} + missing = sorted(registry_dark - declared_registry) + extra = sorted(declared_registry - registry_dark) if missing or extra: problems.append(f"registry coverage mismatch: undeclared={missing}, unregistered={extra}") @@ -652,9 +646,6 @@ def sync_herdr_repo_variants( """Generate managed repository variants for every supported profile. Repository variants only; never select or touch a live configuration. - ``config.night.toml`` is emitted only when the caller supplies a night - palette, so standard dark/light runs keep today's exact behavior (R5, - design §5 row 32). """ changes: list[bool] = [] for profile in profiles: @@ -671,12 +662,6 @@ def sync_herdr_repo_variants( base / "config.light.toml", herdr_content(profile, "light", variants["light"]) ) ) - if "night" in variants: - changes.append( - write_if_changed( - base / "config.night.toml", herdr_content(profile, "night", variants["night"]) - ) - ) return changes @@ -686,7 +671,7 @@ def write_active_repo_file(path: Path, content: str) -> bool: Unlike a live ~/.config target, this path must always be an independent file: an external mode selector, prior manual edit, or a stale artifact from an older sync can leave it as a symlink to one of - its own -dark/-light/-night siblings, and write_if_changed follows + its own -dark/-light siblings, and write_if_changed follows symlinks — so writing the current active mode's content here would silently overwrite whichever sibling the symlink happened to point to (exactly the DreamcoderKitty/dreamcoder-ui.conf corruption fixed @@ -721,7 +706,7 @@ def sync_repo_snippets(variants: dict[str, dict[str, str]], active: dict[str, st ) # Kitty UI active file (variants handled by VARIANT_REGISTRY above). - # This path can be left as a symlink to one of the -dark/-light/-night + # This path can be left as a symlink to one of the -dark/-light # siblings by an external mode selector; write_if_changed would follow # that symlink and overwrite the sibling's own content instead of this # file's, so unlink it first to guarantee an independent write target. @@ -751,12 +736,6 @@ def sync_repo_snippets(variants: dict[str, dict[str, str]], active: dict[str, st hypr_content(variants["light"]), ) ) - repo_changes.append( - write_active_repo_file( - ROOT / "DreamcoderThemes/dreamcoder/hyprland-night.conf", - hypr_content(variants["night"]), - ) - ) repo_changes += write_variant_files( ROOT / "DreamcoderThemes/dreamcoder", {k: f"hypr-colors-{v}.lua" for k, v in D.items()}, @@ -783,12 +762,6 @@ def sync_repo_snippets(variants: dict[str, dict[str, str]], active: dict[str, st waybar_content(variants["light"]), ) ) - repo_changes.append( - write_active_repo_file( - ROOT / "DreamcoderThemes/dreamcoder/waybar-night.css", - waybar_content(variants["night"]), - ) - ) # Rofi — per-mode + active repo_changes.append( @@ -803,12 +776,6 @@ def sync_repo_snippets(variants: dict[str, dict[str, str]], active: dict[str, st rofi_content(variants["light"]), ) ) - repo_changes.append( - write_active_repo_file( - ROOT / "DreamcoderThemes/dreamcoder/rofi-night.rasi", - rofi_content(variants["night"]), - ) - ) # Desktop/WM active files (no suffix — tracks current mode) repo_changes.append( @@ -881,12 +848,6 @@ def sync_repo_snippets(variants: dict[str, dict[str, str]], active: dict[str, st zellij_content(variants["light"], "dreamcoder-light"), ) ) - repo_changes.append( - write_active_repo_file( - ROOT / "DreamcoderZellij/.config/zellij/dreamcoder-night.kdl", - zellij_content(variants["night"], "dreamcoder-night"), - ) - ) # Herdr repository variants are intentionally separate from live selection. repo_changes += sync_herdr_repo_variants(variants) @@ -959,28 +920,16 @@ def print_summary( print(f"Repo variant/snippet changes: {sum(repo_changes)}") -def _generation_profile() -> str: - """Resolve the repo-generation render profile for this invocation. - - Task 4.3 replaced the Phase-3 env-only hook with the persisted - ``render_profile()`` resolver: ``DREAMCODER_THEME_PROFILE`` (process-only, - never mutates) -> persisted ``theme.render_profile`` -> schema default - ``standard`` (design §3). - """ - return render_profile() - - def render_coverage_plan( paths: Any, active: dict[str, str], mode: str, - profile: str, variants: dict[str, dict[str, str]], ) -> dict[str, str]: - """Render content for every one of the 32 coverage consumers in memory. + """Render content for every one of the 33 coverage consumers in memory. Pure rendering: no writer, selector, or filesystem mutation (R5, ADR-004). - This is the preparation boundary's in-memory 32-target render — the subject + This is the preparation boundary's in-memory 33-target render — the subject of the coverage assertion and the render-variant plan the activation transaction commits. Renderers still receive only ``dict[str, str]``. """ @@ -1014,57 +963,39 @@ def render_coverage_plan( "waybar_matugen": waybar_matugen_content(active), "rofi": rofi_content(active), "rofi_matugen": rofi_matugen_content(active), - # Named-profile selector consumers (design §5 rows 13/6/10/32): the - # selector line and the repo-only artifacts are rendered here too. - "zellij": ( - 'theme "dreamcoder-night"\n' if profile == "night" else f'theme "dreamcoder-{mode}"\n' - ), - "codex_app": opencode_content(variants["night"]), - "antigravity": antigravity_content(variants["night"]), + # Selector and repository-only consumers (design §5 rows 13/6/10/32), + # rendered exactly as sync_repo_snippets() writes them: the Codex app + # active file follows the active palette, Antigravity's active file is + # pinned to Dark, and Herdr renders the base mode's variant. + "zellij": f'theme "dreamcoder-{mode}"\n', + "codex_app": opencode_content(active), + "antigravity": antigravity_content(variants["dark"]), } complete = next((p for p in SUPPORTED_PROFILES if p is not None and p.is_complete), None) - plan["herdr"] = herdr_content(complete, "night", variants["night"]) if complete else "" + plan["herdr"] = herdr_content(complete, mode, variants[mode]) if complete else "" return plan -def prepare(base: str, profile: str) -> PreparedSync: +def prepare(base: str) -> PreparedSync: """Validate-first preparation boundary (design §4, ADR-004; task 5.1). - Loads canonical variants/guardrails/profile parameters, resolves the - base+profile pair, adapts, transforms (Night), validates the final palette - with the independent WCAG 2.2 + APCA dual gate, renders all 33 coverage - consumers in memory, and asserts the coverage declaration — with ZERO - filesystem writes. ``main()`` and the CLI activation transaction commit the - returned immutable plan; a failed gate raises ``ThemeGateError`` before any - writer, selector, or settings mutation (R4/R8). + Loads canonical variants/guardrails, adapts the base palette, validates + it with the independent WCAG 2.2 + APCA dual gate, renders all 33 + coverage consumers in memory, and asserts the coverage declaration — + with ZERO filesystem writes. ``main()`` and the CLI activation + transaction commit the returned immutable plan; a failed gate raises + ``ThemeGateError`` before any writer, selector, or settings mutation + (R4/R8). """ if base not in {"light", "dark"}: raise SystemExit(f"base mode must be 'light' or 'dark' (got '{base}')") - if profile not in VALID_RENDER_PROFILES: - raise SystemExit(f"render profile must be 'standard' or 'night' (got '{profile}')") - if profile == "night" and base != "dark": - raise SystemExit( - f"render profile 'night' requires base mode 'dark' (got '{base}'): " - "Night always derives from the Dreamcoder Dark base (ADR-003)." - ) paths = theme_paths() guardrails = load_guardrails(paths.tokens_file) - params = load_render_profile(paths.tokens_file) variants = load_variants(DEFAULT_VARIANTS, paths.tokens_file) - # Night is a derived render variant (ADR-003): canonical dark + the - # deterministic transform. Registry and explicit branches consume it through - # the same dict[str, str] renderer shape (ADR-004). - variants["night"] = night_palette(dict(variants["dark"]), params, guardrails) - - if profile == "night": - adapted = adaptive_palette(variants["dark"], "dark", paths.wallpaper, adaptive_enabled()) - active = night_palette(adapted, params, guardrails) - else: - adapted = adaptive_palette(variants[base], base, paths.wallpaper, adaptive_enabled()) - active = adapted - - gate_errors = validate_palette(active, guardrails, profile=profile, mode=base) + active = adaptive_palette(variants[base], base, paths.wallpaper, adaptive_enabled()) + + gate_errors = validate_palette(active, guardrails, mode=base) if gate_errors: raise ThemeGateError(gate_errors) @@ -1072,7 +1003,7 @@ def prepare(base: str, profile: str) -> PreparedSync: if coverage_problems: raise ThemeGateError(coverage_problems, kind="Coverage gate failed") - render_plan = render_coverage_plan(paths, active, base, profile, variants) + render_plan = render_coverage_plan(paths, active, base, variants) missing = [row.consumer_id for row in COVERAGE if row.consumer_id not in render_plan] if missing: raise ThemeGateError( @@ -1082,7 +1013,6 @@ def prepare(base: str, profile: str) -> PreparedSync: return PreparedSync( mode=base, - profile=profile, active=active, variants=variants, coverage=COVERAGE, @@ -1095,33 +1025,18 @@ def main() -> None: if gen.is_file(): subprocess.run([sys.executable, str(gen)], check=True) paths = theme_paths() - profile = _generation_profile() - # Night always resolves the Dreamcoder Dark base; otherwise the - # Light/Dark base comes from theme_mode() (unchanged responsibility). - base = "dark" if profile == "night" else theme_mode() + base = theme_mode() # Validation-first preparation (R4, design §4): the final palette must pass # the independent WCAG 2.2 + APCA dual gate and the 33-consumer coverage # assertion BEFORE any writer or selector runs. A failed gate exits non-zero - # with zero writes and no profile/settings mutation. + # with zero writes and no settings mutation. try: - prepared = prepare(base, profile) + prepared = prepare(base) except ThemeGateError as exc: raise SystemExit(str(exc)) from None - if profile == "night": - # Repository-only Night generation: the 32-target coverage is produced - # through sync_repo_snippets(); live active paths and the active bat - # theme dir are left untouched (CLI activation owns them, Phase 5). - repo_changes = ( - sync_repo_snippets(prepared.variants, prepared.active) if write_repo_enabled() else [] - ) - changed: dict[str, bool] = {} - print_summary(prepared.mode, paths, changed, repo_changes) - print("Night repository generation only — active outputs untouched (PR3)") - return - - changed = sync_active_targets(paths, prepared.active, prepared.mode, prepared.profile) + changed = sync_active_targets(paths, prepared.active, prepared.mode) bat_variant_changes = sync_bat_theme_variants(paths, prepared.variants) repo_changes = ( sync_repo_snippets(prepared.variants, prepared.active) if write_repo_enabled() else [] @@ -1132,9 +1047,9 @@ def main() -> None: raise SystemExit(f"Generated Starship config is invalid: {paths.starship}") print_summary(prepared.mode, paths, changed, repo_changes) - # Best-effort, outside the 33-consumer coverage contract: Orca has no - # Night profile of its own and lives entirely in the user's live - # settings file, so a failure here must never abort the rest of sync. + # Best-effort, outside the 33-consumer coverage contract: Orca lives + # entirely in the user's live settings file, so a failure here must + # never abort the rest of sync. try: orca_status = sync_orca_theme(prepared.active) except OSError as exc: diff --git a/src/dreamcoder_theme/targets.py b/src/dreamcoder_theme/targets.py index 2c5af5cc..549a7a5f 100644 --- a/src/dreamcoder_theme/targets.py +++ b/src/dreamcoder_theme/targets.py @@ -134,9 +134,7 @@ def _validate_records(records: list[Any]) -> None: if "dusk" in modes: raise ManifestError(f"dusk is not a runtime render mode: {target_id}") if modes != set(ALL_RENDER_VARIANTS): - raise ManifestError( - f"render variants must be dark, light and derived night: {target_id}" - ) + raise ManifestError(f"render variants must be dark and light: {target_id}") for output in render["repository_outputs"].values(): if output in outputs: raise ManifestError(f"duplicate repository output: {output}") diff --git a/src/dreamcoder_theme/writers.py b/src/dreamcoder_theme/writers.py index cf527ae5..6b8b48cc 100644 --- a/src/dreamcoder_theme/writers.py +++ b/src/dreamcoder_theme/writers.py @@ -111,22 +111,16 @@ def ensure_kitty_ui_include(path: Path) -> bool: return True -def update_ghostty_theme(path: Path, mode: str, profile: str = "standard") -> bool: +def update_ghostty_theme(path: Path, mode: str) -> bool: """Update Ghostty config to use the correct theme name, opacity, and blur. - Profile-aware selector (design §6): Night maps to ``dreamcoder-night``; - the legacy ``dreamcoder`` name is retained only for standard light; - standard dark keeps ``dreamcoder-dark``. + The legacy ``dreamcoder`` name is retained only for light; dark keeps + ``dreamcoder-dark``. """ if not path.exists(): return False content = path.read_text() - if profile == "night": - theme_name = "dreamcoder-night" - elif mode != "light": - theme_name = "dreamcoder-dark" - else: - theme_name = "dreamcoder" + theme_name = "dreamcoder-dark" if mode != "light" else "dreamcoder" changed = False @@ -144,29 +138,12 @@ def update_ghostty_theme(path: Path, mode: str, profile: str = "standard") -> bo return write_if_changed(path, content) if changed else False -def update_zellij_config( - path: Path, mode: str, profile: str = "standard", kdl_ready: bool = True -) -> bool: - """Patch Zellij config.kdl to select the theme for base mode + profile. - - Night writes ``theme "dreamcoder-night"`` only when its KDL artifact - exists in the prepared plan (design §6): passing ``kdl_ready=False`` - fails closed rather than pointing at a missing theme (no silent - standard-dark substitution, R5). - """ +def update_zellij_config(path: Path, mode: str) -> bool: + """Patch Zellij config.kdl to select ``theme "dreamcoder-{mode}"``.""" if not path.exists(): return False content = path.read_text() - if profile == "night": - theme_name = "dreamcoder-night" - if not kdl_ready: - raise ValueError( - 'cannot select theme "dreamcoder-night": its KDL artifact is ' - "not present in the prepared plan (fail closed, no standard-" - "dark substitution)" - ) - else: - theme_name = f"dreamcoder-{mode}" + theme_name = f"dreamcoder-{mode}" new_line = f'theme "{theme_name}"' pattern = re.compile(r'^\s*theme\s+".*?"\s*$', re.MULTILINE) @@ -180,14 +157,9 @@ def update_zellij_config( return write_if_changed(path, content) -def update_warp_settings(path: Path, mode: str, profile: str = "standard") -> bool: - """Patch Warp settings.toml with mode-aware opacity/blur for glass coherence. - - Appearance class resolves from base/profile (design §6): Night keeps the - dark opacity/blur behavior and never enters the light branch. - """ - dark_appearance = mode == "dark" or profile == "night" - if dark_appearance: +def update_warp_settings(path: Path, mode: str) -> bool: + """Patch Warp settings.toml with mode-aware opacity/blur for glass coherence.""" + if mode == "dark": opacity_val = 76 blur_val = 20 blur_texture = True @@ -232,9 +204,8 @@ def write_variant_files( """Write every named variant, failing closed before the first write. The declared ``names`` must be a subset of the provided ``variants`` map - (dark/light/night) so a caller can never silently fall back to standard - dark for a missing Night palette (R5, design §6): the preflight raises - before any file is touched. + (dark/light) so a caller can never silently fall back to another palette + for a missing variant: the preflight raises before any file is touched. """ missing = set(names) - set(variants) if missing: @@ -260,7 +231,7 @@ def write_variant_files_and_active( All content is rendered in memory before the first write, and ``write_variant_files``' fail-closed ``names <= variants`` preflight runs - before any file is touched — a missing Night variant or a render error + before any file is touched — a missing variant or a render error aborts with zero mutations. Full snapshot/rollback of the activation transaction is owned by the Phase 5 activation layer. """ diff --git a/tests/characterization/capture_legacy_sync.py b/tests/characterization/capture_legacy_sync.py index e1ca290c..18082f79 100644 --- a/tests/characterization/capture_legacy_sync.py +++ b/tests/characterization/capture_legacy_sync.py @@ -1,16 +1,15 @@ """Characterization harness for the current (legacy) sync plan. -Hexagonal-architecture-v2 Phase 0 tasks 0.2 + 0.3: capture, for all 32 +Hexagonal-architecture-v2 Phase 0 tasks 0.2 + 0.3: capture, for all 33 consumers declared by ``sync.py:COVERAGE``, the resolved active path, repository variant paths per mode, mode set, writer/selector behavior, summary rows (via the real ``print_summary``), and the rendered content hashes produced by the current ``prepare()``/render path — WITHOUT running any writer, selector, or filesystem mutation. -Variant paths are derived from the authoritative ``COVERAGE.night_artifact`` -row by mode substitution ("night" -> dark/light, "Night" -> Dark/Light), -which matches every current repository artifact naming pattern; only Zellij -(current code writes the night KDL only) carries an explicit override. +Variant paths are derived from the authoritative ``COVERAGE.artifact`` (Dark) +row by mode substitution ("dark" -> light, "Dark" -> Light), which matches +every current repository artifact naming pattern. Outputs (deterministic, sorted): - tests/fixtures/legacy_sync_characterization.json @@ -53,6 +52,7 @@ hypr_content, kitty_content, kitty_ui_content, + lazygit_content, ls_colors_content, nvim_content, obsidian_content, @@ -71,7 +71,7 @@ from dreamcoder_theme.settings import ROOT, theme_paths # noqa: E402 from dreamcoder_theme.sync import COVERAGE, prepare, print_summary # noqa: E402 -MODES = ("dark", "light", "night") +MODES = ("dark", "light") # Builders used by the current sync path for repository variant artifacts. BUILDERS = { @@ -86,6 +86,7 @@ "pi_theme": pi_theme_content, "antigravity": antigravity_content, "tmux": tmux_content, + "lazygit": lazygit_content, "zsh_syntax": zsh_syntax_content, "ls_colors": ls_colors_content, "bat": bat_content, @@ -119,6 +120,7 @@ "pi_theme": "pi_theme", "antigravity": None, "tmux": "tmux", + "lazygit": "lazygit", "zsh_syntax": "zsh_syntax", "ls_colors": "ls_colors", "bat": "bat", @@ -142,12 +144,6 @@ "herdr": None, } -# Consumers whose current repo artifact is written for only a subset of modes. -# Zellij: only the night KDL is generated today (dark/light have no artifact). -MODE_ARTIFACT_OVERRIDES: dict[str, dict[str, str | None]] = { - "zellij": {"dark": None, "light": None}, -} - def rel(p: Path) -> str: """Deterministic POSIX path relative to the repository root.""" @@ -159,25 +155,21 @@ def rel(p: Path) -> str: def variant_paths_for(row) -> dict[str, str | None]: - """Per-mode repository artifact paths from the COVERAGE night artifact.""" - artifact = row.night_artifact + """Per-mode repository artifact paths from the COVERAGE (Dark) artifact.""" + artifact = row.artifact if artifact.startswith("active:"): return {m: artifact for m in MODES} - override = MODE_ARTIFACT_OVERRIDES.get(row.consumer_id, {}) out: dict[str, str | None] = {} for mode in MODES: - if mode in override: - out[mode] = override[mode] - else: - resolved = artifact - if "" in resolved: - complete = next( - (p for p in SUPPORTED_PROFILES if p is not None and p.is_complete), None - ) - resolved = resolved.replace( - "", complete.evidence.version if complete else "unknown" - ) - out[mode] = resolved.replace("night", mode).replace("Night", mode.title()) + resolved = artifact + if "" in resolved: + complete = next( + (p for p in SUPPORTED_PROFILES if p is not None and p.is_complete), None + ) + resolved = resolved.replace( + "", complete.evidence.version if complete else "unknown" + ) + out[mode] = resolved.replace("dark", mode).replace("Dark", mode.title()) return out @@ -207,7 +199,7 @@ def capture_characterization() -> dict: "writer": row.writer, "selection_strategy": row.selection_strategy, "source": row.source, - "night_artifact": row.night_artifact, + "artifact": row.artifact, "modes": list(MODES), "active_path": active_path_for(row, paths), "variant_paths": variant_paths_for(row), @@ -281,7 +273,7 @@ def artifact_hash_for(row, base_mode: str, prepared) -> dict: def capture_output_hashes() -> dict: artifacts: dict[str, dict[str, dict]] = {} for base_mode in ("dark", "light"): - prepared = prepare(base_mode, "standard") + prepared = prepare(base_mode) for row in COVERAGE: artifacts.setdefault(row.consumer_id, {})[base_mode] = artifact_hash_for( row, base_mode, prepared diff --git a/tests/ml4w/ml4w_managed.bats b/tests/ml4w/ml4w_managed.bats index 2eb52f0c..e78430d3 100644 --- a/tests/ml4w/ml4w_managed.bats +++ b/tests/ml4w/ml4w_managed.bats @@ -157,7 +157,7 @@ load_ml4w_lib() { @test "ml4w lib: a symlink to a Dreamcoder variant is accepted" { mkdir -p "${HOME}/.config/hypr" - ln -s "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/hypr-colors-night.lua" "${HOME}/.config/hypr/colors.lua" + ln -s "${DREAMCODER_DOTS_DIR}/DreamcoderThemes/dreamcoder/hypr-colors-light.lua" "${HOME}/.config/hypr/colors.lua" load_ml4w_lib run hypr_colors_is_dreamcoder "${HOME}/.config/hypr/colors.lua" diff --git a/tests/shell/test_apply_theme.bats b/tests/shell/test_apply_theme.bats index 7755a8eb..6943c30e 100644 --- a/tests/shell/test_apply_theme.bats +++ b/tests/shell/test_apply_theme.bats @@ -26,50 +26,28 @@ teardown() { [ "$status" -eq 1 ] } -@test "apply-theme-mode.sh with invalid profile exits non-zero" { - run bash scripts/apply-theme-mode.sh dark "" dusk 2>&1 +@test "apply-theme-mode.sh rejects night as a mode" { + run bash scripts/apply-theme-mode.sh night 2>&1 [ "$status" -eq 1 ] - [[ "$output" == *"Invalid render profile"* ]] + [[ "$output" == *"Invalid mode: night"* ]] } -@test "apply-theme-mode.sh night profile: missing artifact blocks with zero mutation" { - export HOME="${TEST_DIR}/home" - mkdir -p "${HOME}/.config/waybar" - ln -sf colors-dark.css "${HOME}/.config/waybar/colors.css" - - run bash scripts/apply-theme-mode.sh dark "" night 2>&1 - [ "$status" -eq 1 ] - [[ "$output" == *"Night preparation failed"* ]] - # Zero mutation pre-preparation: symlink untouched, cursor-cli env absent. - [ "$(readlink "${HOME}/.config/waybar/colors.css")" = "colors-dark.css" ] - [ ! -e "${HOME}/.cache/dreamcoder/cursor-cli.env" ] -} - -@test "apply-theme-mode.sh selects night artifacts when profile=night" { - # Manual trace: the selector code paths must resolve the *-night - # artifacts through VARIANT while DREAMCODER_THEME_MODE stays dark. +@test "apply-theme-mode.sh selects mode artifacts with no render profile" { + # Every selector resolves the *-dark / *-light artifacts straight from + # MODE; the retired Night profile and its artifacts are gone. script="scripts/apply-theme-mode.sh" - grep -q 'VARIANT="${MODE}"' "$script" - grep -q '\[\[ "${PROFILE}" == "night" \]\] && VARIANT="night"' "$script" - grep -q 'ln -sf "colors-${VARIANT}.css"' "$script" - grep -q 'ln -sf "colors-${VARIANT}.rasi"' "$script" - grep -q 'ln -sf "colors-${VARIANT}.lua"' "$script" - grep -q 'ln -sf "hypr-colors-${VARIANT}.lua"' "$script" - grep -q '"${_link%.conf}-${VARIANT}.conf"' "$script" - grep -q 'Dreamcoder-${VARIANT^}.yaml' "$script" - grep -q 'ln -sf "dreamcoder-${VARIANT}.theme"' "$script" - grep -qF 'theme \"dreamcoder-${VARIANT}\"' "$script" - grep -q 'delta-dreamcoder-${VARIANT}.gitconfig' "$script" - grep -q 'config.${VARIANT}.yml' "$script" - grep -q 'LAZYGIT_LINK=' "$script" - grep -q 'DreamcoderLazygit/.config/lazygit/config.night.yml' "$script" - grep -q 'DREAMCODER_THEME_PROFILE="${PROFILE}"' "$script" - grep -q 'REQUIRED_NIGHT_ARTIFACTS' "$script" -} - -@test "apply-theme-mode.sh kanagawa bridge carries night-derived colors" { - grep -q '@ukiyo-color-text "#beccd8"' scripts/apply-theme-mode.sh - grep -q '@ukiyo-color-bg-pane "#07090f"' scripts/apply-theme-mode.sh + grep -q 'ln -sf "colors-${MODE}.css"' "$script" + grep -q 'ln -sf "colors-${MODE}.rasi"' "$script" + grep -q 'ln -sf "colors-${MODE}.lua"' "$script" + grep -q 'ln -sf "hypr-colors-${MODE}.lua"' "$script" + grep -q '"${_link%.conf}-${MODE}.conf"' "$script" + grep -q 'Dreamcoder-${MODE^}.yaml' "$script" + grep -q 'ln -sf "dreamcoder-${MODE}.theme"' "$script" + grep -qF 'theme \"dreamcoder-${MODE}\"' "$script" + grep -q 'delta-dreamcoder-${MODE}.gitconfig' "$script" + grep -q 'config.${MODE}.yml' "$script" + ! grep -qi 'night' "$script" + ! grep -q 'PROFILE=' "$script" } @test "apply-theme-mode.sh flips the live lazygit config to the current variant" { @@ -78,17 +56,10 @@ teardown() { # existing delta selector), never at the static mode-tracking config.yml. script="scripts/apply-theme-mode.sh" grep -q 'LAZYGIT_LINK="${HOME}/.config/lazygit/config.yml"' "$script" - grep -q 'LAZYGIT_VARIANT="${DOTS_DIR}/DreamcoderLazygit/.config/lazygit/config.${VARIANT}.yml"' "$script" + grep -q 'LAZYGIT_VARIANT="${DOTS_DIR}/DreamcoderLazygit/.config/lazygit/config.${MODE}.yml"' "$script" grep -qF 'ln -sf "${LAZYGIT_VARIANT}" "${LAZYGIT_LINK}"' "$script" } -@test "apply-theme-mode.sh night gate requires the generated lazygit night artifact" { - script="scripts/apply-theme-mode.sh" - grep -q 'DreamcoderLazygit/.config/lazygit/config.night.yml' "$script" - run bash -n "$script" - [ "$status" -eq 0 ] -} - @test "apply-theme-mode.sh re-selects the Dreamcoder btop theme after ML4W resets it" { # ML4W upgrades rewrite btop.conf with color_theme = "matugen"; every apply # must point it back at the Dreamcoder theme without rewriting it needlessly. diff --git a/tests/shell/test_dreamcoder_dispatch.bats b/tests/shell/test_dreamcoder_dispatch.bats index 6a42f3d7..c650856d 100644 --- a/tests/shell/test_dreamcoder_dispatch.bats +++ b/tests/shell/test_dreamcoder_dispatch.bats @@ -50,6 +50,13 @@ assert report["upstreams"]["ml4w"]["mappings"] == [], report [[ "$output" == *"upstream-diff"* ]] } +@test "dreamcoder rejects night as an unknown command" { + run bash "${TEST_DIR}/scripts/dreamcoder" night + [ "$status" -eq 2 ] + [[ "$output" == *"Usage: dreamcoder"* ]] + [[ "$output" != *"night"* ]] +} + @test "dreamcoder exposes src/ to Python routes without an installed package" { mkdir -p "${TEST_DIR}/src/dreamcoder_theme" printf 'def main():\n print("sync-ok")\n' > "${TEST_DIR}/src/dreamcoder_theme/sync.py" diff --git a/tests/shell/test_pi_theme.bats b/tests/shell/test_pi_theme.bats index 522b3e6e..77a694b6 100644 --- a/tests/shell/test_pi_theme.bats +++ b/tests/shell/test_pi_theme.bats @@ -1,7 +1,7 @@ #!/usr/bin/env bats # ============================================================================ -# Tests for DreamcoderPi/.pi/agent/scripts/pi-theme.sh (profile-aware selector, -# design §5 row 9, task 4.6) +# Tests for DreamcoderPi/.pi/agent/scripts/pi-theme.sh (Light/Dark selector, +# design §5 row 9) # ============================================================================ setup() { @@ -17,34 +17,34 @@ setup() { cp DreamcoderPi/.pi/agent/scripts/pi-theme.sh \ "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" touch "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-dark.json" - touch "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json" + touch "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-light.json" } teardown() { rm -rf "${TEST_DIR}" } -@test "pi-theme.sh selects night artifact when profile=night" { - run env DREAMCODER_THEME_PROFILE=night bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 - [ "$status" -eq 0 ] - [ "$(readlink "${PI_AGENT_DIR}/themes/dreamcoder.json")" = "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json" ] -} - -@test "pi-theme.sh keeps dark artifact for standard profile" { +@test "pi-theme.sh selects the dark artifact in dark mode" { run bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 [ "$status" -eq 0 ] [ "$(readlink "${PI_AGENT_DIR}/themes/dreamcoder.json")" = "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-dark.json" ] } -@test "pi-theme.sh rejects an invalid profile" { - run env DREAMCODER_THEME_PROFILE=dusk bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 - [ "$status" -eq 1 ] - [[ "$output" == *"invalid render profile"* ]] +@test "pi-theme.sh selects the light artifact in light mode" { + run env DREAMCODER_THEME_MODE=light bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 + [ "$status" -eq 0 ] + [ "$(readlink "${PI_AGENT_DIR}/themes/dreamcoder.json")" = "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-light.json" ] } -@test "pi-theme.sh fails closed when night artifact is missing" { - rm "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-night.json" +@test "pi-theme.sh ignores a stale DREAMCODER_THEME_PROFILE" { run env DREAMCODER_THEME_PROFILE=night bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 + [ "$status" -eq 0 ] + [ "$(readlink "${PI_AGENT_DIR}/themes/dreamcoder.json")" = "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-dark.json" ] +} + +@test "pi-theme.sh fails closed when the mode artifact is missing" { + rm "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/themes/dreamcoder-dark.json" + run bash "${DREAMCODER_DOTS_DIR}/DreamcoderPi/.pi/agent/scripts/pi-theme.sh" 2>&1 [ "$status" -eq 1 ] [[ "$output" == *"not found"* ]] } diff --git a/tests/test_canonical_loaders.py b/tests/test_canonical_loaders.py index 38ae6b06..0711ac10 100644 --- a/tests/test_canonical_loaders.py +++ b/tests/test_canonical_loaders.py @@ -1,8 +1,7 @@ """Canonical loader tests (task 2.5): thresholds come from tokens, never code. -`load_guardrails()` and `load_render_profile()` read the canonical Night -parameters and guardrail floors from `tokens.json` and fail closed when a -required key is missing — no policy literals are permitted on the runtime path +`load_guardrails()` reads the canonical guardrail floors from `tokens.json` +and fails closed when a required key is missing — no policy literals are permitted on the runtime path (ADR-002, R2/R3). """ @@ -14,8 +13,6 @@ from dreamcoder_theme._math import apca_lc from dreamcoder_theme.palette import ( load_guardrails, - load_render_profile, - night_palette, validate_palette, ) @@ -85,44 +82,8 @@ def test_gate_uses_loaded_threshold_not_a_literal(tmp_path): tokens["guardrails"]["minimum_apca_quiet"] = 55 g = load_guardrails(_write_tokens(tmp_path, tokens)) pal = dict(tokens["modes"]["dark"]) - errors = validate_palette(pal, g, profile="standard", mode="dark") + errors = validate_palette(pal, g, mode="dark") quiet_errors = [e for e in errors if "class=quiet" in e and "minimum_apca_quiet" in e] assert quiet_errors, "quiet floor change was not picked up from loaded tokens" assert "=55" in quiet_errors[0] assert abs(apca_lc(pal["subtle"], pal["bg"])) < 55 - - -def test_load_render_profile_returns_canonical_night_parameters(): - params = load_render_profile(THEME_ROOT / "tokens.json") - assert params == { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - } - - -def test_load_render_profile_fails_when_profile_missing(tmp_path): - tokens = _tokens() - del tokens["render_profiles"]["night"] - with pytest.raises(ValueError, match="render profile 'night' missing"): - load_render_profile(_write_tokens(tmp_path, tokens)) - - -def test_load_render_profile_fails_on_invalid_bounds(tmp_path): - tokens = _tokens() - tokens["render_profiles"]["night"]["corrective_step"] = 0.2 # > maximum_corrective_delta 0.12 - with pytest.raises(ValueError, match="corrective_step"): - load_render_profile(_write_tokens(tmp_path, tokens)) - - -def test_night_transform_uses_loaded_profile_parameters(tmp_path): - """A canonical brightness change must change the derived palette (no literals).""" - tokens = _tokens() - tokens["render_profiles"]["night"]["brightness_factor"] = 0.9 - params = load_render_profile(_write_tokens(tmp_path, tokens)) - g = load_guardrails(THEME_ROOT / "tokens.json") - base = dict(tokens["modes"]["dark"]) - night_0_86 = night_palette(base, load_render_profile(THEME_ROOT / "tokens.json"), g) - night_0_90 = night_palette(base, params, g) - assert night_0_90["surface1"] != night_0_86["surface1"] diff --git a/tests/test_cli_theme_activation.py b/tests/test_cli_theme_activation.py index 5fb9e061..7e99591c 100644 --- a/tests/test_cli_theme_activation.py +++ b/tests/test_cli_theme_activation.py @@ -1,7 +1,8 @@ """CLI theme activation + transaction/rollback (Phase 5, tasks 5.2/5.3/5.5/5.7). -Covers R7/R4 scenarios: ``theme apply`` persists base+profile, light/dark exit -Night, a failing gate changes nothing and exits non-zero, and an injected +Covers R7/R4 scenarios: ``theme apply`` persists the Light/Dark base, ``night`` +is no longer an accepted choice, a failing gate changes nothing and exits +non-zero, and an injected post-commit write/reload failure restores file bytes, symlink targets, and prior settings. Tests drive ``control.main`` directly (no subprocess) with an isolated temp config home; the system/reload adapter is mocked. @@ -85,7 +86,6 @@ def theme_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / ".cache")) monkeypatch.setenv("DREAMCODER_WRITE_REPO", "0") monkeypatch.setenv("DREAMCODER_CLEAN_OPENCODE_THEMES", "0") - monkeypatch.delenv("DREAMCODER_THEME_PROFILE", raising=False) monkeypatch.delenv("DREAMCODER_THEME_MODE", raising=False) monkeypatch.delenv("DREAMCODER_SYNC_DONE", raising=False) monkeypatch.delenv("DREAMCODER_WALLPAPER", raising=False) @@ -122,54 +122,53 @@ def _tree_state(root: Path) -> dict[str, str]: # --------------------------------------------------------------------------- -# R7: Night activation +# R7: Light/Dark activation # --------------------------------------------------------------------------- -def test_night_activation_persists_profile_and_dark_base(theme_home: Path, capsys) -> None: - rc, payload = _run("night", capsys) +def test_dark_activation_persists_base(theme_home: Path, capsys) -> None: + rc, payload = _run("dark", capsys) assert rc == 0 assert settings_get("terminal.default_mode") == "dark" - assert settings_get("theme.render_profile") == "night" - assert payload["requested"] == "night" + assert payload["requested"] == "dark" assert payload["effective_base"] == "dark" - assert payload["effective_profile"] == "night" + assert "effective_profile" not in payload assert payload["coverage"] == "33/33" assert payload["rollback_state"] == "none" assert payload["changed"]["kitty"] is True - # The dark base + Night profile produce an active kitty output. assert theme_paths().kitty.exists() -# --------------------------------------------------------------------------- -# R7: Light and Dark exit Night -# --------------------------------------------------------------------------- - - -def test_light_from_night_persists_standard_and_regenerates_all_active( - theme_home: Path, capsys -) -> None: - _run("night", capsys) - night_kitty = theme_paths().kitty.read_bytes() +def test_light_from_dark_regenerates_all_active(theme_home: Path, capsys) -> None: + _run("dark", capsys) + dark_kitty = theme_paths().kitty.read_bytes() rc, payload = _run("light", capsys) assert rc == 0 assert settings_get("terminal.default_mode") == "light" - assert settings_get("theme.render_profile") == "standard" - assert payload["effective_profile"] == "standard" assert payload["coverage"] == "33/33" assert payload["rollback_state"] == "none" - light_kitty = theme_paths().kitty.read_bytes() - assert light_kitty != night_kitty # active outputs were regenerated + assert theme_paths().kitty.read_bytes() != dark_kitty # active outputs regenerated -def test_dark_from_night_persists_standard(theme_home: Path, capsys) -> None: - _run("night", capsys) - rc, payload = _run("dark", capsys) +def test_night_choice_is_rejected(theme_home: Path, capsys) -> None: + before = _tree_state(theme_home) + with pytest.raises(SystemExit) as exc: + control.main(["theme", "apply", "night", "--json"]) + assert exc.value.code == 2 + assert "invalid choice" in capsys.readouterr().err + assert _tree_state(theme_home) == before + + +def test_activation_ignores_and_preserves_legacy_render_profile(theme_home: Path, capsys) -> None: + """A settings file from the Night era still activates cleanly.""" + settings_file = theme_home / ".config/dreamcoder/settings.json" + settings_file.parent.mkdir(parents=True) + settings_file.write_text(json.dumps({"theme": {"render_profile": "night"}})) + rc, payload = _run("light", capsys) assert rc == 0 - assert settings_get("terminal.default_mode") == "dark" - assert settings_get("theme.render_profile") == "standard" - assert payload["effective_profile"] == "standard" - assert payload["coverage"] == "33/33" + assert payload["effective_base"] == "light" + assert settings_get("terminal.default_mode") == "light" + assert settings_get("theme.render_profile") == "night" # preserved, never read # --------------------------------------------------------------------------- @@ -182,12 +181,12 @@ def test_failing_gate_changes_no_settings_or_output_and_exits_nonzero( ) -> None: before = _tree_state(theme_home) with mock.patch("dreamcoder_theme.sync.validate_palette", return_value=["forced gate failure"]): - rc, payload = _run("night", capsys) + rc, payload = _run("dark", capsys) assert rc != 0 assert payload["rollback_state"] == "rejected" assert payload["errors"] == ["forced gate failure"] assert _tree_state(theme_home) == before # zero writes - assert settings_get("theme.render_profile") is None # nothing persisted + assert settings_get("terminal.default_mode") is None # nothing persisted # --------------------------------------------------------------------------- @@ -198,7 +197,7 @@ def test_failing_gate_changes_no_settings_or_output_and_exits_nonzero( def test_post_commit_reload_failure_restores_bytes_symlinks_and_settings( theme_home: Path, capsys ) -> None: - # Seed prior state: light/standard with a matugen bridge symlink exactly as + # Seed prior state: light with a matugen bridge symlink exactly as # the shell adapter would leave it (colors.css -> colors-light.css). _run("light", capsys) paths = theme_paths() @@ -209,13 +208,12 @@ def test_post_commit_reload_failure_restores_bytes_symlinks_and_settings( os.symlink("colors-light.css", paths.waybar_matugen) before = _tree_state(theme_home) - rc, payload = _run("night", capsys, reload_failure=True) + rc, payload = _run("dark", capsys, reload_failure=True) assert rc != 0 assert payload["rollback_state"] == "restored" assert _tree_state(theme_home) == before assert os.readlink(paths.waybar_matugen) == "colors-light.css" assert bridge.read_text() == real_content.decode() - assert settings_get("theme.render_profile") == "standard" assert settings_get("terminal.default_mode") == "light" @@ -234,11 +232,11 @@ def failing_writer(path: Path, content: str) -> bool: return True with mock.patch("dreamcoder_theme.sync.write_if_changed", side_effect=failing_writer): - rc, payload = _run("night", capsys) + rc, payload = _run("dark", capsys) assert rc != 0 assert payload["rollback_state"] == "restored" assert _tree_state(theme_home) == before - assert settings_get("theme.render_profile") == "standard" + assert settings_get("terminal.default_mode") == "light" # --------------------------------------------------------------------------- @@ -246,17 +244,15 @@ def failing_writer(path: Path, content: str) -> bool: # --------------------------------------------------------------------------- -def test_night_light_dark_end_to_end_transitions(theme_home: Path, capsys) -> None: - transitions: list[tuple[str, str]] = [] - for choice in ("night", "light", "dark"): +def test_dark_light_dark_end_to_end_transitions(theme_home: Path, capsys) -> None: + transitions: list[str] = [] + for choice in ("dark", "light", "dark"): rc, payload = _run(choice, capsys) assert rc == 0 assert payload["coverage"] == "33/33" assert payload["rollback_state"] == "none" - transitions.append( - (settings_get("terminal.default_mode"), settings_get("theme.render_profile")) - ) - assert transitions == [("dark", "night"), ("light", "standard"), ("dark", "standard")] + transitions.append(settings_get("terminal.default_mode")) + assert transitions == ["dark", "light", "dark"] # --------------------------------------------------------------------------- @@ -298,7 +294,7 @@ def test_reload_failure_restores_lazygit_active_and_live_symlink(theme_home: Pat os.symlink("config.dark.yml", live / "config.yml") before = _tree_state(theme_home) - rc, payload = _run("night", capsys, reload_failure=True) + rc, payload = _run("dark", capsys, reload_failure=True) assert rc != 0 assert payload["rollback_state"] == "restored" assert _tree_state(theme_home) == before @@ -333,7 +329,7 @@ def test_reload_failure_restores_herdr_override_selector( os.symlink(old_target, override) monkeypatch.setenv("HERDR_CONFIG_PATH", str(override)) - def fail_after_switch(_base: str, _profile: str) -> None: + def fail_after_switch(_base: str) -> None: override.unlink() os.symlink(new_target, override) raise RuntimeError("injected Herdr reload failure") @@ -350,9 +346,9 @@ def fail_after_switch(_base: str, _profile: str) -> None: def test_generic_settings_interface_keeps_working(theme_home: Path, capsys) -> None: - assert control.main(["settings", "set", "theme.render_profile", "night", "--json"]) == 0 + assert control.main(["settings", "set", "motion.active", "fluid", "--json"]) == 0 capsys.readouterr() - assert settings_get("theme.render_profile") == "night" - assert control.main(["settings", "get", "theme.render_profile", "--json"]) == 0 + assert settings_get("motion.active") == "fluid" + assert control.main(["settings", "get", "motion.active", "--json"]) == 0 out = json.loads(capsys.readouterr().out) - assert out["value"] == "night" + assert out["value"] == "fluid" diff --git a/tests/test_dark_neutral_palette.py b/tests/test_dark_neutral_palette.py index 8e19543d..5ea5581d 100644 --- a/tests/test_dark_neutral_palette.py +++ b/tests/test_dark_neutral_palette.py @@ -9,8 +9,6 @@ apca_lc, contrast, load_guardrails, - load_render_profile, - night_palette, ) ROOT = Path(__file__).resolve().parents[1] @@ -92,13 +90,3 @@ def test_dark_roles_meet_their_declared_apca_thresholds(role: str, guardrail: st thresholds = load_guardrails(TOKENS) assert abs(apca_lc(dark[role], dark["bg"])) >= thresholds[guardrail] - - -def test_night_is_derived_from_the_neutral_dark_source_without_parameter_changes(): - dark = _dark() - night = night_palette(dark, load_render_profile(TOKENS), load_guardrails(TOKENS)) - - assert night["bg"] == "#000000" - assert night["selection"] == night["selection_bg"] - assert night["text"] != dark["text"] - assert contrast(night["text"], night["surface3"]) >= 7 diff --git a/tests/test_dreamcoder_sync.py b/tests/test_dreamcoder_sync.py index b463c505..a1516505 100644 --- a/tests/test_dreamcoder_sync.py +++ b/tests/test_dreamcoder_sync.py @@ -11,7 +11,7 @@ import pytest from dreamcoder_theme import sync -from dreamcoder_theme.palette import load_render_profile, load_variants, night_palette +from dreamcoder_theme.palette import load_variants from dreamcoder_theme.palette_tokens import VARIANTS as V from dreamcoder_theme.settings import ThemePaths @@ -138,13 +138,9 @@ def active() -> dict[str, str]: @pytest.fixture def variants() -> dict[str, dict[str, str]]: - tokens_file = ROOT / "DreamcoderThemes" / "dreamcoder" / "tokens.json" - params = load_render_profile(tokens_file) - guardrails = _canonical_guardrails() return { "dark": dict(V["dark"]), "light": dict(V["light"]), - "night": night_palette(dict(V["dark"]), params, guardrails), } @@ -218,13 +214,6 @@ def _main_patches(mock_paths, active, variants, **overrides): write_repo_enabled=True, valid_starship=True, load_guardrails=_canonical_guardrails(), - load_render_profile={ - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - night_palette=active, batch_theme_variants=[False], ) vals.update(overrides) @@ -238,8 +227,6 @@ def _main_patches(mock_paths, active, variants, **overrides): "write_repo_enabled", "valid_starship", "load_guardrails", - "load_render_profile", - "night_palette", ): if name in vals: result.append(mock.patch(f"dreamcoder_theme.sync.{name}", return_value=vals[name])) @@ -275,9 +262,9 @@ def test_main_gate_failure_blocks_all_writes(mock_paths, active, variants): """R4: a failed dual gate performs zero writes and exits non-zero. No writer, selector, variant, or repo writer may run when validation - fails, and no settings/profile mutation occurs (Phase 2 main() performs - none; persisted profile state is Phase 4/5 — here the fail-closed contract - is: non-zero exit, zero writes, prior profile untouched by construction). + fails, and no settings mutation occurs (main() performs none — here the + fail-closed contract is: non-zero exit, zero writes, prior state untouched + by construction). """ patches = [ mock.patch("dreamcoder_theme.sync.theme_paths", return_value=mock_paths), @@ -287,16 +274,6 @@ def test_main_gate_failure_blocks_all_writes(mock_paths, active, variants): mock.patch("dreamcoder_theme.sync.adaptive_enabled", return_value=False), mock.patch("dreamcoder_theme.sync.write_repo_enabled", return_value=True), mock.patch("dreamcoder_theme.sync.load_guardrails", return_value=_canonical_guardrails()), - mock.patch( - "dreamcoder_theme.sync.load_render_profile", - return_value={ - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - ), - mock.patch("dreamcoder_theme.sync.night_palette", return_value=active), mock.patch("dreamcoder_theme.sync.validate_palette", return_value=["forced gate failure"]), ] # Every write path must be untouched when the gate fails. @@ -480,7 +457,7 @@ def test_kitty_ui_active_write_does_not_corrupt_symlinked_sibling( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, variants, active ) -> None: """An external mode selector can leave dreamcoder-ui.conf as a symlink to - one of the -dark/-light/-night siblings. Writing the active (dark) file + one of the -dark/-light siblings. Writing the active (dark) file through that symlink must never overwrite the sibling's own content.""" monkeypatch.setattr(sync, "ROOT", tmp_path) kitty_dir = tmp_path / "DreamcoderKitty/.config/kitty" diff --git a/tests/test_four_candidate_validation.py b/tests/test_four_candidate_validation.py index 6cad8a71..fcea0013 100644 --- a/tests/test_four_candidate_validation.py +++ b/tests/test_four_candidate_validation.py @@ -1,10 +1,9 @@ -"""Four-candidate deterministic validation tests (task 2.6). +"""Deterministic candidate validation tests (task 2.6). -The dual gate (ADR-002) is exercised against the four deterministic -candidates: standard Light, standard Dark, design-system Dusk, and the derived -Night palette (adaptive disabled for the gate — candidates are built directly -from the canonical token modes, never through ``adaptive_palette``). All four -must pass both the WCAG 2.2 and APCA floors. Per-class at-floor passes and +The dual gate (ADR-002) is exercised against the deterministic candidates: +Light, Dark, and design-system Dusk (adaptive disabled for the gate — +candidates are built directly from the canonical token modes, never through +``adaptive_palette``). All must pass both the WCAG 2.2 and APCA floors. Per-class at-floor passes and just-below-floor failures are asserted for Heading (Lc 60 light / 45 dark), Body (75 light / 50 dark), Quiet (44), UI (60 light / 28 dark), and On-accent (60), using the mode-aware floor keys. @@ -18,8 +17,6 @@ from dreamcoder_theme._math import apca_lc, contrast from dreamcoder_theme.palette import ( load_guardrails, - load_render_profile, - night_palette, validate_palette, ) @@ -60,30 +57,16 @@ def _nearest_gray( # --------------------------------------------------------------------------- -# Four deterministic candidates +# Deterministic candidates # --------------------------------------------------------------------------- -def test_four_candidates_all_pass_the_dual_gate(): +def test_candidates_all_pass_the_dual_gate(): tokens = _tokens() g = _guardrails() - params = load_render_profile(THEME_ROOT / "tokens.json") for mode in ("light", "dark", "dusk"): - errors = validate_palette(dict(tokens["modes"][mode]), g, profile="standard", mode=mode) - assert errors == [], f"standard:{mode} gate failures: {errors}" - night = night_palette(dict(tokens["modes"]["dark"]), params, g) - errors = validate_palette(night, g, profile="night", mode="dark") - assert errors == [], f"derived:night gate failures: {errors}" - - -def test_night_candidate_is_deterministic(): - tokens = _tokens() - params = load_render_profile(THEME_ROOT / "tokens.json") - base = dict(tokens["modes"]["dark"]) - first = night_palette(base, params, _guardrails()) - second = night_palette(base, params, _guardrails()) - assert first == second - assert validate_palette(first, _guardrails(), profile="night", mode="dark") == [] + errors = validate_palette(dict(tokens["modes"][mode]), g, mode=mode) + assert errors == [], f"{mode} gate failures: {errors}" def test_candidates_use_canonical_palettes_not_adaptive_output(): @@ -114,7 +97,7 @@ def _assert_class_floor(mode: str, fg_key: str, floor_key: str, pair_bg: str | N assert above_lc >= floor and above_wcag >= 4.5 pal_above = dict(pal) pal_above[fg_key] = above - errors = validate_palette(pal_above, g, profile="standard", mode=mode) + errors = validate_palette(pal_above, g, mode=mode) assert not [e for e in errors if f"{fg_key}/{pair_bg or 'bg'}" in e], ( f"{mode}.{fg_key} at-floor {above} (Lc {above_lc:.1f} >= {floor}) should pass" ) @@ -123,7 +106,7 @@ def _assert_class_floor(mode: str, fg_key: str, floor_key: str, pair_bg: str | N assert below_lc < floor pal_below = dict(pal) pal_below[fg_key] = below - errors = validate_palette(pal_below, g, profile="standard", mode=mode) + errors = validate_palette(pal_below, g, mode=mode) apca_errors = [e for e in errors if "APCA fail" in e and f"pair={fg_key}/" in e] assert apca_errors, ( f"{mode}.{fg_key} just-below {below} (Lc {below_lc:.1f} < {floor}) must block" diff --git a/tests/test_herdr_switch.py b/tests/test_herdr_switch.py index ac014b47..9d5eaabe 100644 --- a/tests/test_herdr_switch.py +++ b/tests/test_herdr_switch.py @@ -38,7 +38,7 @@ def _variant_root( tmp_path: Path, *, versions: tuple[str, ...] = ("0.7.3", "0.8.0", "0.8.2", "0.9.1"), - modes: tuple[str, ...] = ("dark", "light", "night"), + modes: tuple[str, ...] = ("dark", "light"), ) -> Path: root = tmp_path / "variants" for version in versions: @@ -151,8 +151,8 @@ def test_newer_unprofiled_version_falls_back_to_newest_variant(tmp_path: Path) - def test_installed_091_selects_its_own_variant(tmp_path: Path) -> None: root = _variant_root(tmp_path) - choice = choose_variant("herdr 0.9.1\n", "night", root) - assert choice.path == root / "0.9.1/config.night.toml" + choice = choose_variant("herdr 0.9.1\n", "light", root) + assert choice.path == root / "0.9.1/config.light.toml" assert choice.profile_version == "0.9.1" assert choice.fallback is False @@ -352,7 +352,7 @@ def test_reload_failure_removes_a_newly_created_selector(tmp_path: Path) -> None def test_reload_launch_failure_restores_the_previous_selector(tmp_path: Path) -> None: root = _variant_root(tmp_path) selector = tmp_path / "config.toml" - original = root / "0.9.1/config.night.toml" + original = root / "0.9.1/config.dark.toml" selector.symlink_to(original) run = FakeRun(reload_error=FileNotFoundError("herdr")) outcome = switch_herdr("light", run=run, selector=selector, variant_root=root) diff --git a/tests/test_herdr_theme_generation.py b/tests/test_herdr_theme_generation.py index d1bd0ac5..1e9ab49b 100644 --- a/tests/test_herdr_theme_generation.py +++ b/tests/test_herdr_theme_generation.py @@ -100,9 +100,9 @@ def test_herdr_080_light_renders_dreamcoder_light() -> None: assert parsed["ui"]["accent"] == "#6FA0AF" -@pytest.mark.parametrize("mode", ("dark", "light", "night")) +@pytest.mark.parametrize("mode", ("dark", "light")) def test_herdr_082_uses_mode_aware_base_and_sidebar_tokens(mode: str) -> None: - palette = VARIANTS["dark"] if mode == "night" else VARIANTS[mode] + palette = VARIANTS[mode] parsed = tomllib.loads(herdr_content(HERDR_082_PROFILE, mode, palette)) expected_base = "catppuccin-latte" if mode == "light" else "catppuccin" @@ -142,9 +142,9 @@ def test_herdr_variants_are_byte_stable_and_have_matching_structure() -> None: assert dark != light -@pytest.mark.parametrize("mode", ("dusk", "invalid")) +@pytest.mark.parametrize("mode", ("dusk", "night", "invalid")) def test_herdr_rejects_non_static_modes(mode: str) -> None: - with pytest.raises(HerdrModeError, match="only dark, light, and night"): + with pytest.raises(HerdrModeError, match="only dark and light"): herdr_content(HERDR_073_PROFILE, mode, VARIANTS["dark"]) @@ -180,7 +180,6 @@ def test_repository_sync_writes_only_versioned_variants( def test_checked_in_repository_variants_match_the_renderer() -> None: repo = Path(__file__).parents[1] - night = sync.prepare("dark", "standard").variants["night"] for profile in (HERDR_073_PROFILE, HERDR_080_PROFILE, HERDR_082_PROFILE, HERDR_091_PROFILE): base = repo / "DreamcoderHerdr/.config/herdr/dreamcoder" / profile.evidence.version @@ -190,7 +189,6 @@ def test_checked_in_repository_variants_match_the_renderer() -> None: assert (base / "config.light.toml").read_text() == herdr_content( profile, "light", VARIANTS["light"] ) - assert (base / "config.night.toml").read_text() == herdr_content(profile, "night", night) def test_unsupported_profile_produces_no_repository_files( diff --git a/tests/test_lazygit_renderer.py b/tests/test_lazygit_renderer.py index 2a2cd70f..de94de9c 100644 --- a/tests/test_lazygit_renderer.py +++ b/tests/test_lazygit_renderer.py @@ -3,7 +3,7 @@ Covers the token-driven Lazygit integration: every mode variant parses as valid YAML, theme/author/branch-log colors come from the canonical tokens (never a duplicated per-mode palette), the Delta syntax theme stays valid per mode -(Catppuccin Latte light / Mocha dark+night), non-color Lazygit behavior is +(Catppuccin Latte light / Mocha dark), non-color Lazygit behavior is preserved, and the registry registration is consistent with the engine. """ @@ -16,7 +16,7 @@ import pytest import yaml -from dreamcoder_theme.palette import detect_mode, load_render_profile, night_palette +from dreamcoder_theme.palette import detect_mode from dreamcoder_theme.palette_tokens import VARIANTS from dreamcoder_theme.renderer_registry import REGISTRATIONS from dreamcoder_theme.renderers_lazygit import lazygit_content @@ -26,18 +26,10 @@ TOKENS = json.loads(TOKENS_FILE.read_text()) -def _night() -> dict[str, str]: - params = load_render_profile(TOKENS_FILE) - guardrails = { - k: float(v) for k, v in TOKENS["guardrails"].items() if isinstance(v, (int, float)) - } - return night_palette(dict(VARIANTS["dark"]), params, guardrails) +VARIANTS_BY_MODE = {"dark": VARIANTS["dark"], "light": VARIANTS["light"]} -VARIANTS_BY_MODE = {"dark": VARIANTS["dark"], "light": VARIANTS["light"], "night": _night()} - - -@pytest.fixture(params=["dark", "light", "night"]) +@pytest.fixture(params=["dark", "light"]) def mode(request: pytest.FixtureRequest) -> str: return request.param @@ -63,7 +55,6 @@ def test_active_config_artifact_parses(self) -> None: ROOT / "DreamcoderLazygit/.config/lazygit/config.yml", ROOT / "DreamcoderLazygit/.config/lazygit/config.dark.yml", ROOT / "DreamcoderLazygit/.config/lazygit/config.light.yml", - ROOT / "DreamcoderLazygit/.config/lazygit/config.night.yml", ): assert path.is_file(), f"missing generated artifact {path}" assert _parse(path.read_text())["gui"]["theme"] @@ -124,17 +115,10 @@ def test_dark_uses_catppuccin_mocha(self) -> None: cmd = _parse(lazygit_content(VARIANTS["dark"]))["git"]["diffRenderers"][0]["command"] assert 'delta --syntax-theme "Catppuccin Mocha" --paging=never' in cmd - def test_night_keeps_dark_theme(self) -> None: - cmd = _parse(lazygit_content(VARIANTS_BY_MODE["night"]))["git"]["diffRenderers"][0][ - "command" - ] - assert 'delta --syntax-theme "Catppuccin Mocha" --paging=never' in cmd - def test_installed_catppuccin_themes_are_used(self) -> None: # The chosen themes must match the machine-installed set (Latte/Mocha). assert detect_mode(VARIANTS["light"]) == "light" assert detect_mode(VARIANTS["dark"]) == "dark" - assert detect_mode(VARIANTS_BY_MODE["night"]) == "dark" # Night keeps dark semantics class TestNonColorBehaviorPreserved: @@ -169,13 +153,13 @@ def test_lazygit_registered_as_active_and_repository(self) -> None: regs = {r.consumer_id: r for r in REGISTRATIONS} assert "lazygit" in regs reg = regs["lazygit"] - assert reg.modes == frozenset({"dark", "light", "night"}) + assert reg.modes == frozenset({"dark", "light"}) assert reg.output_kind == "active-and-repository" assert reg.sync.repository.value == "mode_variants" assert reg.sync.active.value == "resolved_active_path" def test_renderer_conforms_for_all_modes(self) -> None: regs = {r.consumer_id: r for r in REGISTRATIONS} - for mode in ("dark", "light", "night"): + for mode in ("dark", "light"): result = regs["lazygit"].renderer(VARIANTS_BY_MODE[mode]) assert type(result) is str and len(result) > 0 diff --git a/tests/test_night_naming_selection.py b/tests/test_night_naming_selection.py deleted file mode 100644 index 7b9d7faf..00000000 --- a/tests/test_night_naming_selection.py +++ /dev/null @@ -1,250 +0,0 @@ -"""Night naming/selection tests (Phase 4, task 4.6). - -Covers R5/R9: every Night registry entry resolves to a distinct ``*-night`` -artifact name, the Ghostty/Zellij/Warp/Neovim/Pi selectors are profile-aware, -and standard-dark substitution is detected and forbidden. -""" - -from __future__ import annotations - -import re -from pathlib import Path - -import pytest - -from dreamcoder_theme import sync -from dreamcoder_theme.renderers_extra_nvim import nvim_dispatcher_content -from dreamcoder_theme.writers import ( - update_ghostty_theme, - update_warp_settings, - update_zellij_config, - write_variant_files, -) - -# --------------------------------------------------------------------------- -# Every Night registry entry has a distinct *-night name (R5) -# --------------------------------------------------------------------------- - - -def test_every_registry_entry_declares_a_night_name() -> None: - """Each VARIANT_REGISTRY entry must carry a ``night`` key (D = dark/light/night).""" - for base, names, _builder, _active in sync.VARIANT_REGISTRY: - assert "night" in names, f"registry entry {base} has no night name" - - -def test_night_names_never_equal_dark_names() -> None: - """Standard-dark substitution is forbidden: night must not reuse the dark name.""" - for base, names, _builder, _active in sync.VARIANT_REGISTRY: - assert names["night"] != names["dark"], f"night alias for {base} equals dark" - assert names["night"] != names["light"], f"night alias for {base} equals light" - - -def test_night_names_carry_a_night_marker() -> None: - """Every registry night name is visually distinct via -night/-Night or .night. - - Hyphen markers (``*-night``/``*-Night``) are the majority convention; the - dotted marker (``config.night.yml``) follows the established Herdr - ``config.night.toml`` naming for config files whose base name must stay - ``config`` (Lazygit consumes ``config.yml``). - """ - for base, names, _builder, _active in sync.VARIANT_REGISTRY: - name = names["night"] - assert re.search(r"-night\b|-Night\b|\.night\b", name), f"{name} lacks a night marker" - - -def test_registry_coverage_rows_use_the_same_night_names() -> None: - """COVERAGE night_artifact rows match the registry names byte-for-byte.""" - registry_night = { - (base / names["night"]).relative_to(sync.ROOT).as_posix() - for base, names, _builder, _active in sync.VARIANT_REGISTRY - } - declared = {row.night_artifact for row in sync.COVERAGE if row.source == "registry"} - assert declared == registry_night - problems = sync.validate_coverage_declaration() - assert problems == [], problems - - -def test_write_variant_files_writes_night_artifact(tmp_path: Path) -> None: - """write_variant_files produces the night artifact from the night palette.""" - - def builder(variant: dict[str, str]) -> str: - return f"bg={variant['bg']}\n" - - base = tmp_path / "themes" - names = {"dark": "x-dark.conf", "light": "x-light.conf", "night": "x-night.conf"} - variants = { - "dark": {"bg": "#000000"}, - "light": {"bg": "#ffffff"}, - "night": {"bg": "#0d1015"}, - } - write_variant_files(base, names, builder, variants) - assert (base / "x-night.conf").read_text() == "bg=#0d1015\n" - - -def test_write_variant_files_missing_night_fails_closed(tmp_path: Path) -> None: - """A missing night variant aborts before any write (no silent fallback).""" - - def builder(variant: dict[str, str]) -> str: - return f"bg={variant['bg']}\n" - - base = tmp_path / "themes" - names = {"dark": "x-dark.conf", "night": "x-night.conf"} - variants = {"dark": {"bg": "#000000"}} # night missing - with pytest.raises(ValueError, match="night"): - write_variant_files(base, names, builder, variants) - assert not (base / "x-dark.conf").exists() - assert not (base / "x-night.conf").exists() - - -# --------------------------------------------------------------------------- -# Profile-aware selectors (design §6) -# --------------------------------------------------------------------------- - - -class TestGhosttySelector: - def test_night_selects_dreamcoder_night(self, tmp_path: Path) -> None: - path = tmp_path / "config" - path.write_text("[theme]\n") - assert update_ghostty_theme(path, "dark", "night") is True - assert "theme = dreamcoder-night" in path.read_text() - - def test_night_wins_over_light(self, tmp_path: Path) -> None: - path = tmp_path / "config" - path.write_text("[theme]\n") - assert update_ghostty_theme(path, "light", "night") is True - assert "theme = dreamcoder-night" in path.read_text() - assert "theme = dreamcoder\n" not in path.read_text() - - def test_standard_dark_keeps_legacy_name(self, tmp_path: Path) -> None: - path = tmp_path / "config" - path.write_text("[theme]\n") - assert update_ghostty_theme(path, "dark") is True - assert "theme = dreamcoder-dark" in path.read_text() - - def test_standard_light_keeps_legacy_name(self, tmp_path: Path) -> None: - path = tmp_path / "config" - path.write_text("[theme]\n") - assert update_ghostty_theme(path, "light") is True - assert "theme = dreamcoder" in path.read_text() - - def test_night_never_selects_standard_dark(self, tmp_path: Path) -> None: - path = tmp_path / "config" - path.write_text("theme = dreamcoder-dark\n") - assert update_ghostty_theme(path, "dark", "night") is True - content = path.read_text() - assert "theme = dreamcoder-night" in content - assert "theme = dreamcoder-dark" not in content - - -class TestZellijSelector: - def test_night_selects_night_kdl(self, tmp_path: Path) -> None: - path = tmp_path / "config.kdl" - path.write_text("some_setting true\n") - assert update_zellij_config(path, "dark", "night") is True - assert 'theme "dreamcoder-night"' in path.read_text() - - def test_night_fails_closed_when_kdl_not_ready(self, tmp_path: Path) -> None: - path = tmp_path / "config.kdl" - path.write_text("some_setting true\n") - with pytest.raises(ValueError, match="dreamcoder-night"): - update_zellij_config(path, "dark", "night", kdl_ready=False) - # No selector mutation happened. - assert 'theme "dreamcoder-night"' not in path.read_text() - - def test_standard_mode_selector_unchanged(self, tmp_path: Path) -> None: - path = tmp_path / "config.kdl" - path.write_text("some_setting true\n") - assert update_zellij_config(path, "light") is True - assert 'theme "dreamcoder-light"' in path.read_text() - - -class TestWarpSelector: - def test_night_keeps_dark_appearance(self, tmp_path: Path) -> None: - path = tmp_path / "settings.toml" - assert update_warp_settings(path, "dark", "night") is True - content = path.read_text() - assert "override_opacity = 76" in content - assert "override_blur = 20" in content - assert "override_blur_texture = true" in content - - def test_night_never_enters_light_branch(self, tmp_path: Path) -> None: - """Even with a light base, night keeps dark appearance (conflict is caught upstream).""" - path = tmp_path / "settings.toml" - assert update_warp_settings(path, "light", "night") is True - content = path.read_text() - assert "override_opacity = 76" in content - assert "override_opacity = 96" not in content - - def test_standard_light_keeps_light_appearance(self, tmp_path: Path) -> None: - path = tmp_path / "settings.toml" - assert update_warp_settings(path, "light") is True - assert "override_opacity = 96" in path.read_text() - - -class TestNvimSelector: - def test_dispatcher_resolves_profile_before_base_mode(self) -> None: - content = nvim_dispatcher_content() - profile_pos = content.index("DREAMCODER_THEME_PROFILE") - mode_pos = content.index("DREAMCODER_THEME_MODE") - assert profile_pos < mode_pos, "profile must be resolved before base mode" - assert "dreamcoder-night.lua" in content - assert 'profile == "night"' in content - - def test_background_alone_still_selects_standard_dark(self) -> None: - content = nvim_dispatcher_content() - # The dispatcher must never fall back to night when only the base mode - # is known; background=dark resolves to dreamcoder-dark.lua. - assert "dreamcoder-dark.lua" in content - - -class TestPiSelector: - def test_pi_theme_script_is_profile_aware(self) -> None: - script = ( - Path(__file__).resolve().parents[1] / "DreamcoderPi/.pi/agent/scripts/pi-theme.sh" - ).read_text() - assert "DREAMCODER_THEME_PROFILE" in script - assert 'variant="night"' in script or 'variant="${profile}"' in script - assert "dreamcoder-night.json" in script or "dreamcoder-$variant.json" in script - - def test_pi_theme_script_fails_closed_on_invalid_profile(self) -> None: - script = ( - Path(__file__).resolve().parents[1] / "DreamcoderPi/.pi/agent/scripts/pi-theme.sh" - ).read_text() - assert "invalid render profile" in script - - -# --------------------------------------------------------------------------- -# Standard-dark-substitution detection (R5) -# --------------------------------------------------------------------------- - - -def test_apply_theme_script_has_no_standard_dark_night_leak() -> None: - """The shell selector must not substitute standard dark while reporting night. - - The night branch of the Kanagawa bridge and the VARIANT-based artifact - selection must differ from the standard dark values. - """ - script = (Path(__file__).resolve().parents[1] / "scripts/apply-theme-mode.sh").read_text() - # Night selectors are driven by VARIANT, never hardcoded to the dark name. - assert 'VARIANT="${MODE}"' in script - assert '[[ "${PROFILE}" == "night" ]] && VARIANT="night"' in script - # The night Kanagawa palette is distinct from the standard dark palette. - assert '@ukiyo-color-text "#c6c6c6"' in script - assert '@ukiyo-color-text "#E6E6E6"' in script - - -def test_coverage_night_artifacts_all_distinct_from_dark() -> None: - """Every declared Night artifact carries a night marker (no dark reuse). - - The ``opencode`` row is the documented stable-ID exception (design §5 row - 5): the active ``dreamcoder.json`` is overwritten with Night bytes and no - ``dreamcoder-night.json`` sibling exists — its selection strategy carries - the Night identity instead of the artifact name. - """ - for row in sync.COVERAGE: - artifact = row.night_artifact - if row.consumer_id == "opencode" or artifact.startswith("active:"): - assert "night" in row.selection_strategy.lower() - continue - assert re.search(r"-night\b|-Night\b|night", artifact), artifact - assert "dark" not in artifact.split("/")[-1] or "night" in artifact diff --git a/tests/test_night_palette.py b/tests/test_night_palette.py deleted file mode 100644 index ecc184dd..00000000 --- a/tests/test_night_palette.py +++ /dev/null @@ -1,339 +0,0 @@ -"""RED/GREEN tests for the deterministic Night transform (tasks 2.3/2.4). - -Contract under test (design §2, ADR-003): - -- ``night_palette(base, profile_parameters, guardrails) -> dict[str, str]`` - never mutates its input, preserves every palette token key (excluding canonical - mode metadata), and returns lowercase hex. -- The derived display name is "Dreamcoder Dark Night" while - ``details`` stays "darker". -- HSL lightness/saturation are multiplied by the canonical factors with - deterministic integer-rounded RGB; ``rgba()`` tokens preserve alpha exactly. -- Input aliases (e.g. ``selection == selection_bg``) stay exact in the output. -- A bounded corrective pass moves only declared foreground tokens toward the - contrast-safe endpoint, never brightens background/surface roles, and never - exceeds ``maximum_corrective_delta`` of lightness movement. -- New pure ``#000000``/``#ffffff`` values are rejected for functional roles; - explicitly authored policy-approved black canvas/on-color roles are preserved. -- The transform never silently falls back to the standard dark palette. - -The HSL helpers below are test-side color plumbing (a generic conversion, not -the SAPC/APCA formula); the canonical contrast math is imported from the -package. -""" - -import json -from pathlib import Path - -import pytest - -from dreamcoder_theme._math import apca_lc -from dreamcoder_theme.palette import night_palette - -ROOT = Path(__file__).resolve().parents[1] -THEME_ROOT = ROOT / "DreamcoderThemes" / "dreamcoder" - -CANONICAL_PARAMS = { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, -} - - -def _tokens() -> dict: - return json.loads((THEME_ROOT / "tokens.json").read_text()) - - -def _guardrails() -> dict[str, float]: - tokens = _tokens() - return {k: v for k, v in tokens["guardrails"].items() if isinstance(v, (int, float))} - - -def _dark_base(**overrides: str) -> dict[str, str]: - pal = dict(_tokens()["modes"]["dark"]) - pal.update(overrides) - return pal - - -# --------------------------------------------------------------------------- -# Test-side HSL plumbing (generic color conversion; deterministic round-half). -# --------------------------------------------------------------------------- - - -def _rgb(v: str) -> tuple[int, int, int]: - v = v.lstrip("#") - return int(v[0:2], 16), int(v[2:4], 16), int(v[4:6], 16) - - -def _hsl(v: str) -> tuple[float, float, float]: - r, g, b = (c / 255 for c in _rgb(v)) - mx, mn = max(r, g, b), min(r, g, b) - lightness = (mx + mn) / 2 - delta = mx - mn - if delta == 0: - return 0.0, 0.0, lightness - saturation = delta / (1 - abs(2 * lightness - 1)) - if mx == r: - hue = 60 * (((g - b) / delta) % 6) - elif mx == g: - hue = 60 * ((b - r) / delta + 2) - else: - hue = 60 * ((r - g) / delta + 4) - return hue, saturation, lightness - - -def _pure_transform(v: str, params: dict[str, float]) -> str: - """The deterministic HSL-reduction applied by night_palette to one hex.""" - hue, sat, light = _hsl(v) - return _hex_from_hsl( - hue, sat * params["saturation_factor"], light * params["brightness_factor"] - ) - - -def _hex_from_hsl(hue: float, sat: float, light: float) -> str: - c = (1 - abs(2 * light - 1)) * sat - x = c * (1 - abs((hue / 60) % 2 - 1)) - m = light - c / 2 - if hue < 60: - r1, g1, b1 = c, x, 0.0 - elif hue < 120: - r1, g1, b1 = x, c, 0.0 - elif hue < 180: - r1, g1, b1 = 0.0, c, x - elif hue < 240: - r1, g1, b1 = 0.0, x, c - elif hue < 300: - r1, g1, b1 = x, 0.0, c - else: - r1, g1, b1 = c, 0.0, x - r = round((r1 + m) * 255) - g = round((g1 + m) * 255) - b = round((b1 + m) * 255) - return "#" + "".join(f"{max(0, min(255, ch)):02x}" for ch in (r, g, b)) - - -# --------------------------------------------------------------------------- -# Contract tests -# --------------------------------------------------------------------------- - - -def test_night_preserves_token_keys(): - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert set(night) == {key for key, value in base.items() if isinstance(value, str)} - - -def test_night_never_mutates_input(): - base = _dark_base() - snapshot = dict(base) - night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert base == snapshot - - -def test_night_metadata_and_details(): - night = night_palette(_dark_base(), CANONICAL_PARAMS, _guardrails()) - assert night["name"] == "Dreamcoder Dark Night" - assert night["details"] == "darker" - - -def test_night_outputs_lowercase_hex(): - night = night_palette(_dark_base(), CANONICAL_PARAMS, _guardrails()) - for key, value in night.items(): - if value.startswith("#"): - assert value == value.lower(), f"{key} not lowercase: {value}" - - -def test_night_preserves_exact_aliases(): - """selection == selection_bg (and every input alias) stays exact.""" - night = night_palette(_dark_base(), CANONICAL_PARAMS, _guardrails()) - assert night["selection"] == night["selection_bg"] - assert night["subtle"] == night["disabled"] # aliased in canonical dark - assert night["bg"] == night["prompt_bg"] - assert night["text"] == night["on_surface"] - - -def test_night_preserves_rgba_alpha_exactly(): - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - for key in ("panel_rgba", "module_rgba", "active_rgba", "inactive_border", "overlay", "scrim"): - value = base[key] - assert value.startswith("rgba("), key - alpha = value.rsplit(",", 1)[1].rstrip(")") - assert night[key].rsplit(",", 1)[1].rstrip(")") == alpha - assert night[key].endswith(")") - - -def test_night_reduces_lightness_and_saturation(): - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert night["bg"] == base["bg"] - assert _hsl(night["surface1"])[2] < _hsl(base["surface1"])[2] - assert _hsl(night["accent"])[2] < _hsl(base["accent"])[2] - assert _hsl(night["accent"])[1] < _hsl(base["accent"])[1] - - -def test_night_is_byte_identical_across_runs(): - base = _dark_base() - first = night_palette(base, CANONICAL_PARAMS, _guardrails()) - second = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert first == second - - -def test_night_only_preserves_pure_black_for_explicit_oled_roles(): - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - pure_black_keys = {key for key, value in night.items() if value == base["bg"]} - hex_tokens = {value for value in night.values() if value.startswith("#")} - - assert pure_black_keys == {"bg", "prompt_bg", "on_accent", "on_error", "on_focus"} - assert "#ffffff" not in hex_tokens - - -def test_night_preserves_explicit_oled_black_roles(): - base = _dark_base() - for key in ("bg", "prompt_bg", "on_accent", "on_error", "on_focus"): - base[key] = "#000000" - - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - - for key in ("bg", "prompt_bg", "on_accent", "on_error", "on_focus"): - assert night[key] == "#000000" - - -@pytest.mark.parametrize("token", ["#000000"]) -def test_night_rejects_pure_black_on_functional_roles(token): - base = _dark_base() - base["hover"] = token - with pytest.raises(ValueError): - night_palette(base, CANONICAL_PARAMS, _guardrails()) - - -def test_night_transforms_white_away_from_pure_white(): - """A pure-white input is dimmed (L*brightness < 1), never emitted white.""" - base = _dark_base() - base["hover"] = "#ffffff" - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert night["hover"] != "#ffffff" - assert _hsl(night["hover"])[2] < 1.0 - - -def test_night_parameter_bounds_are_enforced(): - base = _dark_base() - g = _guardrails() - bad_params = [ - { - "brightness_factor": 0, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - { - "brightness_factor": 1.01, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 0, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 1.01, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": -0.01, - "corrective_step": 0.02, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.21, - "corrective_step": 0.02, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0, - }, - { - "brightness_factor": 0.86, - "saturation_factor": 0.72, - "maximum_corrective_delta": 0.12, - "corrective_step": 0.02, - "extra": 1, - }, - ] - for params in bad_params: - with pytest.raises(ValueError): - night_palette(base, params, g) - - -def test_night_corrective_movement_is_capped_at_maximum_delta(): - """A token below its floor may be corrected only up to the bound.""" - base = _dark_base(subtle="#2f3a44") - g = _guardrails() - night = night_palette(base, CANONICAL_PARAMS, g) - pure_lightness = _hsl(_pure_transform("#2f3a44", CANONICAL_PARAMS))[2] - moved = _hsl(night["subtle"])[2] - pure_lightness - assert 0 <= moved <= CANONICAL_PARAMS["maximum_corrective_delta"] + 1e-9 - # The deficit is large enough that the bound cannot reach the quiet floor: - assert abs(apca_lc(night["subtle"], night["bg"])) < g["minimum_apca_quiet"] - - -def test_night_bounded_correction_restores_a_floor(): - """A small deficit is fixed within the bound (R4 scenario).""" - base = _dark_base(subtle="#7e8c9e") - g = _guardrails() - night = night_palette(base, CANONICAL_PARAMS, g) - assert abs(apca_lc(night["subtle"], night["bg"])) >= g["minimum_apca_quiet"] - # And the correction respected the bound: lightness moved at most max_delta. - moved = _hsl(night["subtle"])[2] - _hsl(_pure_transform("#7e8c9e", CANONICAL_PARAMS))[2] - assert 0 <= moved <= CANONICAL_PARAMS["maximum_corrective_delta"] + 1e-9 - - -def test_night_never_brightens_backgrounds(): - """Correction touches only declared pairs, never background/surface roles.""" - base = _dark_base(subtle="#2f3a44") - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - for key in ( - "bg", - "bg_soft", - "surface0", - "surface1", - "surface2", - "surface3", - "selection_bg", - "hover", - "pressed", - ): - assert night[key] == _pure_transform(base[key], CANONICAL_PARAMS), ( - f"background {key} was corrected" - ) - - -def test_night_has_no_silent_standard_dark_fallback(): - """The transform never returns the base palette as its output.""" - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert night != base - # An unfixable deficit still yields the bounded transform, never `base`. - broken = _dark_base(subtle="#1a1a1a") - night_broken = night_palette(broken, CANONICAL_PARAMS, _guardrails()) - assert night_broken != broken - - -def test_night_derivation_differs_from_standard_dark(): - """The Night palette must not silently substitute standard dark bytes.""" - base = _dark_base() - night = night_palette(base, CANONICAL_PARAMS, _guardrails()) - assert night["surface1"] != base["surface1"] - assert night["text"] != base["text"] diff --git a/tests/test_palette_dual_gate.py b/tests/test_palette_dual_gate.py index 398487e7..30624388 100644 --- a/tests/test_palette_dual_gate.py +++ b/tests/test_palette_dual_gate.py @@ -48,7 +48,7 @@ def test_wcag_pass_apca_fail_is_blocking(): assert contrast(pal["bg"], pal["diagnostic"]) >= 4.5 assert abs(apca_lc(pal["diagnostic"], pal["bg"])) < _guardrails()["minimum_apca_body_dark"] - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("APCA fail" in e and "diagnostic/bg" in e for e in errors) assert not any("WCAG fail" in e and "diagnostic/bg" in e for e in errors) @@ -61,7 +61,7 @@ def test_apca_pass_wcag_fail_is_blocking(): assert abs(apca_lc(pal["diagnostic"], pal["bg"])) >= _guardrails()["minimum_apca_body_dark"] assert contrast(pal["bg"], pal["diagnostic"]) < 4.5 - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("WCAG fail" in e and "diagnostic/bg" in e for e in errors) assert not any("APCA fail" in e and "diagnostic/bg" in e for e in errors) @@ -74,7 +74,7 @@ def test_light_wcag_pass_apca_fail_is_blocking(): assert contrast(pal["bg"], pal["diagnostic"]) >= 4.5 assert abs(apca_lc(pal["diagnostic"], pal["bg"])) < _guardrails()["minimum_apca_body"] - errors = validate_palette(pal, _guardrails(), profile="standard", mode="light") + errors = validate_palette(pal, _guardrails(), mode="light") assert any("APCA fail" in e and "diagnostic/bg" in e for e in errors) assert not any("WCAG fail" in e and "diagnostic/bg" in e for e in errors) @@ -84,7 +84,7 @@ def test_both_metric_failures_accumulate(): """A pair failing both metrics yields both diagnostics, not a short-circuit.""" pal = _clean_palette("dark") pal["diagnostic"] = "#4a4a4a" # WCAG 2.23 and APCA 11.8 — both fail - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("WCAG fail" in e and "diagnostic/bg" in e for e in errors) assert any("APCA fail" in e and "diagnostic/bg" in e for e in errors) @@ -93,27 +93,26 @@ def test_both_metric_failures_accumulate(): def test_diagnostic_carries_guardrail_key_and_value(): pal = _clean_palette("dark") pal["diagnostic"] = "#7b7b7b" - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") apca_errors = [e for e in errors if "APCA fail" in e and "diagnostic/bg" in e] assert len(apca_errors) == 1 assert "minimum_apca_body_dark" in apca_errors[0] assert "=50" in apca_errors[0] assert "mode=dark" in apca_errors[0] - assert "profile=standard" in apca_errors[0] def test_missing_apca_guardrail_key_fails_closed(): pal = _clean_palette("dark") guardrails = dict(_guardrails()) del guardrails["minimum_apca_body_dark"] - errors = validate_palette(pal, guardrails, profile="standard", mode="dark") + errors = validate_palette(pal, guardrails, mode="dark") assert any("missing guardrail key: minimum_apca_body_dark" in e for e in errors) def test_mode_derives_from_palette_when_omitted(): pal = _clean_palette("dark") pal["diagnostic"] = "#7b7b7b" - errors = validate_palette(pal, _guardrails(), profile="standard") + errors = validate_palette(pal, _guardrails()) assert any("mode=dark" in e for e in errors if "APCA fail" in e) @@ -126,7 +125,7 @@ def test_near_invisible_quiet_pair_fails_wcag_despite_apca_boost(): assert contrast(pal["bg"], pal["subtle"]) < 4.5 assert abs(apca_lc(pal["subtle"], pal["bg"])) >= _guardrails()["minimum_apca_quiet"] - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("WCAG fail" in e and "subtle/bg" in e for e in errors) @@ -136,7 +135,7 @@ def test_every_declared_pair_requires_both_metrics(): floor — an APCA pass alone never waives WCAG (ADR-002).""" pal = _clean_palette("dark") pal["border_ui"] = "#1b1b1b" # APCA 57.1 >= ui_dark 28, WCAG 1.15 < 4.5 - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("WCAG fail" in e and "border_ui/bg" in e for e in errors) assert not any("APCA fail" in e and "border_ui/bg" in e for e in errors) @@ -144,7 +143,7 @@ def test_every_declared_pair_requires_both_metrics(): def test_invalid_mode_is_rejected(): pal = _clean_palette("dark") - errors = validate_palette(pal, _guardrails(), profile="standard", mode="bogus") + errors = validate_palette(pal, _guardrails(), mode="bogus") assert any("invalid mode: bogus" in e for e in errors) @@ -153,7 +152,7 @@ def test_dusk_uses_light_floors(): floors, never the weaker dark floors (design class table).""" pal = _clean_palette("dark") pal["diagnostic"] = "#7b7b7b" # APCA 32.2 < body 75 (light floor) and < body_dark 50 - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dusk") + errors = validate_palette(pal, _guardrails(), mode="dusk") apca_errors = [e for e in errors if "APCA fail" in e and "diagnostic/bg" in e] assert len(apca_errors) == 1 assert "minimum_apca_body" in apca_errors[0] @@ -163,5 +162,5 @@ def test_dusk_uses_light_floors(): def test_missing_declared_pair_token_is_reported(): pal = _clean_palette("dark") pal.pop("text_heading") - errors = validate_palette(pal, _guardrails(), profile="standard", mode="dark") + errors = validate_palette(pal, _guardrails(), mode="dark") assert any("missing token: text_heading (declared heading pair)" in e for e in errors) diff --git a/tests/test_render_profile_settings.py b/tests/test_render_profile_settings.py deleted file mode 100644 index 79278fc4..00000000 --- a/tests/test_render_profile_settings.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Settings contract for ``theme.render_profile`` (Phase 4, tasks 4.1/4.2/4.3). - -Covers R6 scenarios: schema entry with closed values, invalid-value rejection, -unknown-setting preservation, schema default, environment-override precedence -without mutation, persisted resolution, and the ``profile == night -> mode == -dark`` effective-base conflict rule (design §3, ADR-003). -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from dreamcoder_theme import settings as settings_mod -from dreamcoder_theme import settings_store - - -@pytest.fixture -def settings_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Isolate persisted settings under a temp config home.""" - monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / ".config")) - monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / ".local" / "share")) - monkeypatch.setenv("HOME", str(tmp_path)) - monkeypatch.delenv("DREAMCODER_THEME_PROFILE", raising=False) - monkeypatch.delenv("DREAMCODER_THEME_MODE", raising=False) - return tmp_path - - -def _persist(settings_home: Path, key: str, value: str) -> None: - path = settings_home / ".config" / "dreamcoder" / "settings.json" - data = json.loads(path.read_text()) if path.exists() else {} - cursor = data - parts = key.split(".") - for part in parts[:-1]: - cursor = cursor.setdefault(part, {}) - cursor[parts[-1]] = value - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") - - -# --------------------------------------------------------------------------- -# 4.1 RED — schema entry -# --------------------------------------------------------------------------- - - -def test_schema_declares_render_profile_with_standard_default(settings_home: Path) -> None: - """``theme.render_profile`` is a typed schema entry defaulting to standard.""" - spec = settings_store.SETTINGS_SCHEMA["theme.render_profile"] - assert spec["type"] == "string" - assert spec["enum"] == ["standard", "night"] - assert spec["default"] == "standard" - - -def test_render_profile_accepts_standard_and_night(settings_home: Path) -> None: - """Both closed values validate and round-trip through settings_set/get.""" - for value in ("standard", "night"): - assert settings_store.validate_setting_value("theme.render_profile", value) == [] - result = settings_store.settings_set("theme.render_profile", value) - assert result["key"] == "theme.render_profile" - assert settings_store.settings_get("theme.render_profile") == value - - -def test_render_profile_rejects_invalid_value(settings_home: Path) -> None: - """An unknown profile value is rejected by settings_set (never persisted).""" - with pytest.raises(ValueError, match=r"theme\.render_profile must be one of"): - settings_store.settings_set("theme.render_profile", "neon") - assert settings_store.settings_get("theme.render_profile") is None - - -def test_unknown_settings_preserved_with_warnings(settings_home: Path) -> None: - """Unknown settings produce warnings and remain preserved (forward compat).""" - path = settings_home / ".config" / "dreamcoder" / "settings.json" - path.parent.mkdir(parents=True) - path.write_text(json.dumps({"future": {"option": 42}})) - report = settings_store.validate_settings() - assert report["valid"] is True - keys = [warning["key"] for warning in report["warnings"]] - assert "future.option" in keys - assert json.loads(path.read_text()) == {"future": {"option": 42}} - - -# --------------------------------------------------------------------------- -# 4.1/4.3 RED — resolver precedence -# --------------------------------------------------------------------------- - - -def test_default_profile_is_standard(settings_home: Path) -> None: - """Absent env override and absent persisted setting resolve to standard.""" - assert settings_mod.render_profile() == "standard" - - -def test_persisted_profile_is_resolved(settings_home: Path) -> None: - """Persisted ``theme.render_profile=night`` resolves absent an override.""" - _persist(settings_home, "theme.render_profile", "night") - assert settings_mod.render_profile() == "night" - - -def test_env_override_wins_without_mutation( - settings_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Process-only env override wins and never mutates the persisted value.""" - _persist(settings_home, "theme.render_profile", "night") - monkeypatch.setenv("DREAMCODER_THEME_PROFILE", "standard") - assert settings_mod.render_profile() == "standard" - # Persisted value untouched. - assert settings_store.settings_get("theme.render_profile") == "night" - - -def test_env_profile_invalid_fails_closed( - settings_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """An invalid env profile fails closed instead of being interpreted.""" - monkeypatch.setenv("DREAMCODER_THEME_PROFILE", "dusk") - with pytest.raises(SystemExit, match=r"DREAMCODER_THEME_PROFILE must be 'standard' or 'night'"): - settings_mod.render_profile() - - -def test_persisted_invalid_profile_fails_closed(settings_home: Path) -> None: - """An invalid persisted value fails closed, never a runtime profile.""" - _persist(settings_home, "theme.render_profile", "dusk") - with pytest.raises(SystemExit, match=r"theme\.render_profile"): - settings_mod.render_profile() - - -# --------------------------------------------------------------------------- -# 4.3 RED — effective-base resolver (ADR-003 invariant) -# --------------------------------------------------------------------------- - - -def test_effective_base_mode_night_requires_dark( - settings_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """profile=night + mode=dark resolves to dark base.""" - monkeypatch.setenv("DREAMCODER_THEME_MODE", "dark") - monkeypatch.setenv("DREAMCODER_THEME_PROFILE", "night") - assert settings_mod.effective_base_mode() == "dark" - - -def test_effective_base_mode_conflict_fails_actionably( - settings_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """MODE=light + PROFILE=night fails with an actionable error (no Dusk, no coercion).""" - monkeypatch.setenv("DREAMCODER_THEME_MODE", "light") - monkeypatch.setenv("DREAMCODER_THEME_PROFILE", "night") - with pytest.raises(SystemExit, match="requires base mode 'dark'"): - settings_mod.effective_base_mode() - - -def test_effective_base_mode_standard_keeps_light( - settings_home: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - """Standard profile leaves the light/dark base untouched.""" - monkeypatch.setenv("DREAMCODER_THEME_MODE", "light") - assert settings_mod.effective_base_mode() == "light" diff --git a/tests/test_renderer_contract.py b/tests/test_renderer_contract.py index 8baca3cf..5fd638b2 100644 --- a/tests/test_renderer_contract.py +++ b/tests/test_renderer_contract.py @@ -23,8 +23,7 @@ def test_canonical_modes_are_distinct_from_derived_render_variants() -> None: assert {"light", "dusk", "dark"} == CANONICAL_MODES - assert {"light", "dark", "night"} == ALL_RENDER_VARIANTS - assert "night" not in CANONICAL_MODES + assert {"light", "dark"} == ALL_RENDER_VARIANTS assert "dusk" not in ALL_RENDER_VARIANTS @@ -70,7 +69,7 @@ def adapters(cls): TransparentOpenCodeAdapter(), NvimDispatcherAdapter(), NamedZellijAdapter("dreamcoder"), - VersionedHerdrAdapter(complete, "night"), + VersionedHerdrAdapter(complete, "dark"), ) def test_every_adapter_conforms_to_the_one_contract(self, adapters) -> None: diff --git a/tests/test_renderer_registry.py b/tests/test_renderer_registry.py index 9caa4802..d7561c27 100644 --- a/tests/test_renderer_registry.py +++ b/tests/test_renderer_registry.py @@ -55,7 +55,7 @@ def make_registration( consumer_id: str = "test_consumer", renderer: object = kitty_content, contract_version: int = SUPPORTED_CONTRACT_VERSION, - modes: frozenset[str] = frozenset({"dark", "light", "night"}), + modes: frozenset[str] = frozenset({"dark", "light"}), output_kind: str = "active", sync: SyncDefinition | None = None, summary_label: str = "Test consumer", diff --git a/tests/test_renderers_hypr_rgba.py b/tests/test_renderers_hypr_rgba.py index 0e67521d..49d66d78 100644 --- a/tests/test_renderers_hypr_rgba.py +++ b/tests/test_renderers_hypr_rgba.py @@ -21,7 +21,7 @@ def test_light_inactive_border(self) -> None: # 3-digit channels must be clamped to 2 hex digits, not padded. self.assertEqual(_rgba_to_argb("rgba(138, 115, 88, 0.93)"), "rgba(8a7358ed)") - def test_night_inactive_border(self) -> None: + def test_muted_translucent_border(self) -> None: self.assertEqual(_rgba_to_argb("rgba(167, 148, 122, 0.87)"), "rgba(a7947ade)") def test_all_channels_large(self) -> None: diff --git a/tests/test_settings_legacy_render_profile.py b/tests/test_settings_legacy_render_profile.py new file mode 100644 index 00000000..480a5c75 --- /dev/null +++ b/tests/test_settings_legacy_render_profile.py @@ -0,0 +1,91 @@ +"""Legacy ``theme.render_profile`` tolerance after the Night profile removal. + +Dreamcoder ships only Light and Dark. Settings files written by older +versions may still carry ``theme.render_profile`` (``standard``/``night``); +loading them must never crash, and the stale key is treated like any other +unknown setting: reported as a warning and preserved untouched. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from dreamcoder_theme import settings as settings_mod +from dreamcoder_theme import settings_store + + +@pytest.fixture +def settings_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Isolate persisted settings and the mode cache under a temp home.""" + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / ".config")) + monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path / ".cache")) + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / ".local" / "share")) + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.delenv("DREAMCODER_THEME_MODE", raising=False) + return tmp_path + + +def _write_settings(settings_home: Path, data: dict) -> Path: + path = settings_home / ".config" / "dreamcoder" / "settings.json" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") + return path + + +def test_schema_no_longer_declares_render_profile() -> None: + assert "theme.render_profile" not in settings_store.SETTINGS_SCHEMA + assert not hasattr(settings_mod, "render_profile") + + +@pytest.mark.parametrize("legacy_value", ["standard", "night"]) +def test_persisted_legacy_render_profile_loads_as_unknown_setting( + settings_home: Path, legacy_value: str +) -> None: + """A stale key validates (warning only) and is preserved byte-for-byte.""" + path = _write_settings( + settings_home, + {"terminal": {"default_mode": "dark"}, "theme": {"render_profile": legacy_value}}, + ) + before = path.read_bytes() + + report = settings_store.validate_settings() + + assert report["valid"] is True + assert report["errors"] == [] + assert [w["key"] for w in report["warnings"]] == ["theme.render_profile"] + assert settings_store.settings_get("terminal.default_mode") == "dark" + assert settings_store.settings_get("theme.render_profile") == legacy_value + assert path.read_bytes() == before + + +def test_legacy_render_profile_does_not_affect_theme_mode( + settings_home: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A persisted legacy ``night`` value never selects a mode.""" + _write_settings(settings_home, {"theme": {"render_profile": "night"}}) + assert settings_mod.theme_mode() == "dark" + monkeypatch.setenv("DREAMCODER_THEME_MODE", "light") + assert settings_mod.theme_mode() == "light" + + +def test_writing_another_setting_keeps_legacy_key(settings_home: Path) -> None: + """Updating a known key never crashes on, or drops, the stale key.""" + _write_settings(settings_home, {"theme": {"render_profile": "night"}}) + settings_store.settings_set("terminal.default_mode", "light") + assert settings_store.settings_get() == { + "terminal": {"default_mode": "light"}, + "theme": {"render_profile": "night"}, + } + + +def test_unknown_settings_preserved_with_warnings(settings_home: Path) -> None: + """Unknown settings produce warnings and remain preserved (forward compat).""" + path = _write_settings(settings_home, {"future": {"option": 42}}) + report = settings_store.validate_settings() + assert report["valid"] is True + keys = [warning["key"] for warning in report["warnings"]] + assert "future.option" in keys + assert json.loads(path.read_text()) == {"future": {"option": 42}} diff --git a/tests/test_tokens_schema_night.py b/tests/test_tokens_schema_night.py deleted file mode 100644 index 7dbb2c11..00000000 --- a/tests/test_tokens_schema_night.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Schema contract for the canonical `render_profiles.night` parameters (task 2.2). - -`tokens.schema.json` must require `render_profiles.night` and bound its four -parameters: brightness/saturation factors in (0, 1], `maximum_corrective_delta` -in [0, 0.20], and `corrective_step` in (0, maximum_corrective_delta]. JSON -Schema cannot express the relative `corrective_step <= maximum_corrective_delta` -bound, so the schema bounds `corrective_step` to (0, 0.20] and the cross-field -invariant is enforced by the runtime loader (task 2.5) and asserted here. -""" - -import copy -import json -from pathlib import Path - -import jsonschema -import pytest - -ROOT = Path(__file__).resolve().parents[1] -THEME_ROOT = ROOT / "DreamcoderThemes" / "dreamcoder" - -NIGHT_KEYS = ( - "brightness_factor", - "saturation_factor", - "maximum_corrective_delta", - "corrective_step", -) - - -@pytest.fixture(scope="module") -def schema() -> dict: - return json.loads((THEME_ROOT / "tokens.schema.json").read_text()) - - -@pytest.fixture(scope="module") -def tokens() -> dict: - return json.loads((THEME_ROOT / "tokens.json").read_text()) - - -@pytest.fixture(scope="module") -def validator(schema: dict) -> jsonschema.Draft202012Validator: - return jsonschema.Draft202012Validator(schema) - - -def test_render_profiles_required_in_schema(schema): - assert "render_profiles" in schema["required"] - - -def test_render_profiles_requires_night(schema): - assert "night" in schema["properties"]["render_profiles"]["required"] - - -def test_night_requires_all_four_parameters(schema): - required = set(schema["properties"]["render_profiles"]["properties"]["night"]["required"]) - assert set(NIGHT_KEYS) <= required - - -def test_tokens_json_validates_against_schema(validator, tokens): - validator.validate(tokens) - - -def test_tokens_json_contains_canonical_night_parameters(tokens): - night = tokens["render_profiles"]["night"] - assert night["brightness_factor"] == 0.86 - assert night["saturation_factor"] == 0.72 - assert night["maximum_corrective_delta"] == 0.12 - assert night["corrective_step"] == 0.02 - - -def test_missing_render_profiles_fails_validation(schema, tokens): - broken = copy.deepcopy(tokens) - del broken["render_profiles"] - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_night_without_night_profile_fails_validation(schema, tokens): - broken = copy.deepcopy(tokens) - del broken["render_profiles"]["night"] - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -@pytest.mark.parametrize("key", NIGHT_KEYS) -def test_night_missing_parameter_fails_validation(schema, tokens, key): - broken = copy.deepcopy(tokens) - del broken["render_profiles"]["night"][key] - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_brightness_factor_must_be_in_open_unit_interval(schema, tokens): - for bad in (0, 1.01): - broken = copy.deepcopy(tokens) - broken["render_profiles"]["night"]["brightness_factor"] = bad - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_saturation_factor_must_be_in_open_unit_interval(schema, tokens): - for bad in (0, 1.5): - broken = copy.deepcopy(tokens) - broken["render_profiles"]["night"]["saturation_factor"] = bad - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_maximum_corrective_delta_bounds(schema, tokens): - for bad in (-0.01, 0.21): - broken = copy.deepcopy(tokens) - broken["render_profiles"]["night"]["maximum_corrective_delta"] = bad - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_corrective_step_bounds(schema, tokens): - for bad in (0, 0.25): - broken = copy.deepcopy(tokens) - broken["render_profiles"]["night"]["corrective_step"] = bad - with pytest.raises(jsonschema.ValidationError): - jsonschema.Draft202012Validator(schema).validate(broken) - - -def test_corrective_step_must_not_exceed_maximum_corrective_delta(tokens): - """Cross-field bound the schema cannot express: step (0, max_delta]. - - The runtime loader (task 2.5) enforces the same invariant fail-closed. - """ - night = tokens["render_profiles"]["night"] - assert 0 < night["corrective_step"] <= night["maximum_corrective_delta"]