From 4cefd8e7d33cfafff1a24ad3bcaa7ae50cb570fb Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 13:41:51 -0500 Subject: [PATCH 1/7] fix(theme): detach the relaunched waybar from the caller's streams restart_waybar left the new bar holding the caller's stdout, so theme apply (captured by Python or systemd) never returned. --- lib/hyprland.sh | 4 +++- tests/shell/test_lib_hyprland.bats | 36 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/shell/test_lib_hyprland.bats diff --git a/lib/hyprland.sh b/lib/hyprland.sh index 76b272e3..df004636 100644 --- a/lib/hyprland.sh +++ b/lib/hyprland.sh @@ -13,7 +13,9 @@ restart_waybar() { pkill waybar 2>/dev/null || true sleep 0.3 local launch_script="${HOME}/.config/waybar/launch.sh" - [[ -f "${launch_script}" ]] && "${launch_script}" 2>/dev/null || true + # Detach every stream: the relaunched bar outlives us, and an inherited + # stdout pipe would block any caller capturing our output (theme apply). + [[ -f "${launch_script}" ]] && "${launch_script}" /dev/null 2>&1 || true fi } diff --git a/tests/shell/test_lib_hyprland.bats b/tests/shell/test_lib_hyprland.bats new file mode 100644 index 00000000..d42f3d3d --- /dev/null +++ b/tests/shell/test_lib_hyprland.bats @@ -0,0 +1,36 @@ +#!/usr/bin/env bats +# ============================================================================ +# Tests for lib/hyprland.sh +# ============================================================================ + +setup() { + TEST_HOME="$(mktemp -d)" + mkdir -p "${TEST_HOME}/.config/waybar" "${TEST_HOME}/bin" + # Fake launcher that leaves a long-lived background child, like waybar. + cat >"${TEST_HOME}/.config/waybar/launch.sh" <<'SH' +#!/usr/bin/env bash +sleep 30 & +echo $! >"${HOME}/bar.pid" +echo launched +SH + chmod +x "${TEST_HOME}/.config/waybar/launch.sh" + printf '#!/bin/sh\nexit 0\n' >"${TEST_HOME}/bin/pkill" + chmod +x "${TEST_HOME}/bin/pkill" +} + +teardown() { + [[ -f "${TEST_HOME}/bar.pid" ]] && kill "$(cat "${TEST_HOME}/bar.pid")" 2>/dev/null || true + rm -rf "${TEST_HOME}" +} + +@test "restart_waybar never hands its stdout to the relaunched bar" { + # A caller capturing output (theme apply under Python/systemd) must not + # block until the bar exits because the bar inherited the capture pipe. + run timeout 10 bash -c ' + export HOME="'"${TEST_HOME}"'" PATH="'"${TEST_HOME}"'/bin:${PATH}" + source lib/checks.sh; source lib/hyprland.sh + is_gui_session() { return 0; } + out="$(restart_waybar)"; echo "done:${out}"' + [ "$status" -eq 0 ] + [ "$output" = "done:" ] +} From 7d2e47396e2af4ae8fe71cd459358b9d241463b6 Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 13:42:11 -0500 Subject: [PATCH 2/7] fix(theme): re-select the dreamcoder btop theme on every apply ML4W upgrades reset btop.conf color_theme to matugen; nothing restored it. --- scripts/apply-theme-mode.sh | 6 ++++++ tests/shell/test_apply_theme.bats | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/scripts/apply-theme-mode.sh b/scripts/apply-theme-mode.sh index 2d625757..687e18e3 100755 --- a/scripts/apply-theme-mode.sh +++ b/scripts/apply-theme-mode.sh @@ -294,6 +294,12 @@ BTOP_THEME="${HOME}/.config/btop/themes/dreamcoder.theme" if [[ -L "${BTOP_THEME}" ]]; then ln -sf "dreamcoder-${VARIANT}.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. +BTOP_CONF="${HOME}/.config/btop/btop.conf" +if [[ -f "${BTOP_THEME}" && -f "${BTOP_CONF}" ]] && ! grep -q '^color_theme = "dreamcoder"$' "${BTOP_CONF}"; then + sed -i 's/^color_theme = .*/color_theme = "dreamcoder"/' "${BTOP_CONF}" +fi # Zellij: update theme in config.kdl ZELLIJ_CONF="${HOME}/.config/zellij/config.kdl" diff --git a/tests/shell/test_apply_theme.bats b/tests/shell/test_apply_theme.bats index d3a61f0c..7755a8eb 100644 --- a/tests/shell/test_apply_theme.bats +++ b/tests/shell/test_apply_theme.bats @@ -88,3 +88,12 @@ teardown() { 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. + script="scripts/apply-theme-mode.sh" + grep -q 'BTOP_CONF="${HOME}/.config/btop/btop.conf"' "$script" + grep -qF "grep -q '^color_theme = \"dreamcoder\"\$' \"\${BTOP_CONF}\"" "$script" + grep -qF "sed -i 's/^color_theme = .*/color_theme = \"dreamcoder\"/' \"\${BTOP_CONF}\"" "$script" +} From 452c3d321d6d71983fd6525c6322c89baa4fc28f Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 13:46:58 -0500 Subject: [PATCH 3/7] fix(theme): resolve the base mode from the persisted live mode theme_mode() only read DREAMCODER_THEME_MODE and defaulted to dark, so a bare dreamcoder sync (hooks, listeners, shells without the export) rendered Dreamcoder Dark over a system running Light. Fall back to ~/.cache/dreamcoder/cursor-cli.env, the record every mode switch writes, before the dark default. --- src/dreamcoder_theme/settings.py | 30 ++++++++++++++++-- tests/test_theme_mode_resolution.py | 48 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/test_theme_mode_resolution.py diff --git a/src/dreamcoder_theme/settings.py b/src/dreamcoder_theme/settings.py index 323fecec..fd2c7545 100644 --- a/src/dreamcoder_theme/settings.py +++ b/src/dreamcoder_theme/settings.py @@ -75,10 +75,34 @@ class ThemePaths: ) +def persisted_theme_mode() -> str | None: + """Return the base mode apply-theme-mode.sh persisted, or None. + + ``~/.cache/dreamcoder/cursor-cli.env`` is the live mode record every + mode switch writes (Neovim reads the same file as its fallback). + """ + cache_home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache")) + try: + lines = (cache_home / "dreamcoder" / "cursor-cli.env").read_text().splitlines() + except OSError: + return None + for line in lines: + key, _, value = line.removeprefix("export ").partition("=") + if key.strip() == "DREAMCODER_THEME_MODE": + mode = value.strip().strip("\"'").lower() + return mode if mode in {"dark", "light"} else None + return None + + def theme_mode() -> str: - # Dreamcoder Dark is the repo default; a clean env (CI, fresh - # shell) must not silently regenerate the legacy light palette. - mode = os.environ.get("DREAMCODER_THEME_MODE", "dark").lower() + # Precedence: explicit env override, then the persisted live mode, then + # Dreamcoder Dark as the repo default. Without the persisted fallback a + # bare `dreamcoder sync` rendered Dark over a system running Light; a clean + # env (CI, fresh shell) still never regenerates the legacy light palette. + env_mode = os.environ.get("DREAMCODER_THEME_MODE") + if env_mode is None: + return persisted_theme_mode() or "dark" + mode = env_mode.lower() if mode not in {"dark", "light"}: raise SystemExit("DREAMCODER_THEME_MODE must be 'dark' or 'light'") return mode diff --git a/tests/test_theme_mode_resolution.py b/tests/test_theme_mode_resolution.py new file mode 100644 index 00000000..9da2fe04 --- /dev/null +++ b/tests/test_theme_mode_resolution.py @@ -0,0 +1,48 @@ +"""theme_mode() must follow the persisted active mode when no env override exists. + +A bare ``dreamcoder sync`` (from a hook, a listener, or a shell that never +exported DREAMCODER_THEME_MODE) used to render Dreamcoder Dark while the system +was in Light, because the only fallback was the repo default. +""" + +from __future__ import annotations + +import pytest + +from dreamcoder_theme.settings import theme_mode + + +@pytest.fixture +def cache_home(tmp_path, monkeypatch): + monkeypatch.delenv("DREAMCODER_THEME_MODE", raising=False) + monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) + return tmp_path + + +def write_mode_cache(cache_home, body: str) -> None: + target = cache_home / "dreamcoder" / "cursor-cli.env" + target.parent.mkdir(parents=True) + target.write_text(body) + + +def test_env_override_wins_over_persisted_mode(cache_home, monkeypatch): + write_mode_cache(cache_home, 'export DREAMCODER_THEME_MODE="light"\n') + monkeypatch.setenv("DREAMCODER_THEME_MODE", "dark") + assert theme_mode() == "dark" + + +def test_persisted_mode_is_used_without_env(cache_home): + write_mode_cache( + cache_home, + 'export COLORFGBG="0;15"\nexport DREAMCODER_THEME_MODE="light"\n', + ) + assert theme_mode() == "light" + + +def test_defaults_to_dark_without_env_or_cache(cache_home): + assert theme_mode() == "dark" + + +def test_invalid_persisted_mode_falls_back_to_dark(cache_home): + write_mode_cache(cache_home, 'export DREAMCODER_THEME_MODE="sepia"\n') + assert theme_mode() == "dark" From 0ccd8a8c139754883c7497eced48358a7a3412d2 Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 13:47:56 -0500 Subject: [PATCH 4/7] fix(shell): start fish in the persisted live theme mode 05-dreamcoder-theme.fish trusted an inherited DREAMCODER_THEME_MODE (or assumed dark), so panes from a long-lived parent opened with dark LS_COLORS/EZA_COLORS over the Light terminal until the first prompt. --- .../fish/conf.d/05-dreamcoder-theme.fish | 7 ++++ tests/shell/test_fish_theme_mode.bats | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/shell/test_fish_theme_mode.bats diff --git a/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish b/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish index a6d349d5..24728345 100644 --- a/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish +++ b/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish @@ -2,6 +2,13 @@ # Sources generated theme files from themes/dreamcoder/ (synced from tokens.json). # No hardcoded colors — the .sh files are the single source of truth. +# The persisted live mode wins over an inherited value: a long-lived parent +# (e.g. a Herdr server started before the last mode switch) exports a stale one. +set -l mode_cache "$HOME/.cache/dreamcoder/cursor-cli.env" +if test -f "$mode_cache" + set -l persisted (string match -rg '^(?:export )?DREAMCODER_THEME_MODE="?(light|dark)"?$' <"$mode_cache") + test -n "$persisted"; and set -gx DREAMCODER_THEME_MODE $persisted[1] +end set -q DREAMCODER_THEME_MODE; or set -gx DREAMCODER_THEME_MODE dark set -q COLORTERM; or set -gx COLORTERM truecolor set -gx FORCE_COLOR 3 diff --git a/tests/shell/test_fish_theme_mode.bats b/tests/shell/test_fish_theme_mode.bats new file mode 100644 index 00000000..8578735e --- /dev/null +++ b/tests/shell/test_fish_theme_mode.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats +# ============================================================================ +# 05-dreamcoder-theme.fish must start in the persisted live mode, even when the +# parent process exported a stale DREAMCODER_THEME_MODE. +# ============================================================================ + +setup() { + TEST_HOME="$(mktemp -d)" + mkdir -p "${TEST_HOME}/.cache/dreamcoder" + CONF="${PWD}/DreamcoderShell/.config/fish/conf.d/05-dreamcoder-theme.fish" +} + +teardown() { + rm -rf "${TEST_HOME}" +} + +mode_after_startup() { + HOME="${TEST_HOME}" DREAMCODER_DOTS_DIR="${PWD}" fish --no-config -c "source '${CONF}'; echo \$DREAMCODER_THEME_MODE" +} + +@test "persisted light mode overrides a stale inherited dark mode" { + printf 'export DREAMCODER_THEME_MODE="light"\n' >"${TEST_HOME}/.cache/dreamcoder/cursor-cli.env" + DREAMCODER_THEME_MODE=dark run mode_after_startup + [ "$output" = "light" ] +} + +@test "inherited mode is kept when nothing was persisted" { + DREAMCODER_THEME_MODE=light run mode_after_startup + [ "$output" = "light" ] +} + +@test "defaults to dark without env or persisted mode" { + run env -u DREAMCODER_THEME_MODE bash -c "$(declare -f mode_after_startup); TEST_HOME='${TEST_HOME}' CONF='${CONF}' mode_after_startup" + [ "$output" = "dark" ] +} From 8926c75afa8dfc7ef9f4c2350df9649ba0ee65f9 Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 13:53:33 -0500 Subject: [PATCH 5/7] fix(theme): lift broken-link and sticky-dir ls colours to wcag aa or rendered text on the warning swatch (2.09:1 light, 1.34:1 dark) and st used surface1 (4.31:1 light). Invert or onto the base bg and move st to surface0; a test now gates every LS_COLORS entry at 4.5:1 in Light and Dark. --- .../dreamcoder/ls-colors-dreamcoder-dark.sh | 2 +- .../dreamcoder/ls-colors-dreamcoder-light.sh | 2 +- .../dreamcoder/ls-colors-dreamcoder-night.sh | 2 +- DreamcoderThemes/ls-colors-dreamcoder.sh | 2 +- src/dreamcoder_theme/renderers_extra_shell.py | 5 +- tests/test_ls_colors_contrast.py | 55 +++++++++++++++++++ 6 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 tests/test_ls_colors_contrast.py diff --git a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-dark.sh b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-dark.sh index 32d5d3e9..948d96d8 100644 --- a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-dark.sh +++ b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-dark.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash set -euo pipefail # Dreamcoder Dark — LS_COLORS / eza -export LS_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:or=48;2;251;191;36;38;2;230;230;230:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:su=48;2;0;0;0;38;2;212;181;253:sg=48;2;0;0;0;38;2;212;181;253:tw=38;2;165;180;252;48;2;11;11;11:ow=38;2;165;180;252;48;2;11;11;11:st=38;2;165;180;252;48;2;13;13;15:*.sh=38;2;212;181;253:*.bash=38;2;212;181;253:*.zsh=38;2;212;181;253:*.fish=38;2;212;181;253:*.py=38;2;212;181;253:*.rb=38;2;212;181;253:*.rs=38;2;212;181;253:*.go=38;2;212;181;253:*.ts=38;2;212;181;253:*.js=38;2;212;181;253:*.jpg=38;2;216;180;254:*.jpeg=38;2;216;180;254:*.png=38;2;216;180;254:*.gif=38;2;216;180;254:*.bmp=38;2;216;180;254:*.svg=38;2;216;180;254:*.webp=38;2;216;180;254:*.ico=38;2;216;180;254:*.mp3=38;2;216;180;254:*.wav=38;2;216;180;254:*.flac=38;2;216;180;254:*.ogg=38;2;216;180;254:*.m4a=38;2;216;180;254:*.mp4=38;2;216;180;254:*.mkv=38;2;216;180;254:*.webm=38;2;216;180;254:*.mov=38;2;216;180;254:*.pdf=38;2;199;199;199:*.doc=38;2;199;199;199:*.docx=38;2;199;199;199:*.odt=38;2;199;199;199:*.xls=38;2;199;199;199:*.xlsx=38;2;199;199;199:*.ppt=38;2;199;199;199:*.pptx=38;2;199;199;199:*.txt=38;2;199;199;199:*.cfg=38;2;199;199;199:*.conf=38;2;199;199;199:*.json=38;2;199;199;199:*.yaml=38;2;199;199;199:*.yml=38;2;199;199;199:*.toml=38;2;199;199;199:*.xml=38;2;199;199;199:*.css=38;2;199;199;199:*.html=38;2;199;199;199:*.c=38;2;199;199;199:*.h=38;2;199;199;199:*.cpp=38;2;199;199;199:*.hpp=38;2;199;199;199:*.swp=38;2;199;199;199:*.swo=38;2;199;199;199:*.bak=38;2;199;199;199:*.orig=38;2;199;199;199:*.md=#A5B4FC' +export LS_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:or=48;2;251;191;36;38;2;0;0;0:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:su=48;2;0;0;0;38;2;212;181;253:sg=48;2;0;0;0;38;2;212;181;253:tw=38;2;165;180;252;48;2;11;11;11:ow=38;2;165;180;252;48;2;11;11;11:st=38;2;165;180;252;48;2;11;11;11:*.sh=38;2;212;181;253:*.bash=38;2;212;181;253:*.zsh=38;2;212;181;253:*.fish=38;2;212;181;253:*.py=38;2;212;181;253:*.rb=38;2;212;181;253:*.rs=38;2;212;181;253:*.go=38;2;212;181;253:*.ts=38;2;212;181;253:*.js=38;2;212;181;253:*.jpg=38;2;216;180;254:*.jpeg=38;2;216;180;254:*.png=38;2;216;180;254:*.gif=38;2;216;180;254:*.bmp=38;2;216;180;254:*.svg=38;2;216;180;254:*.webp=38;2;216;180;254:*.ico=38;2;216;180;254:*.mp3=38;2;216;180;254:*.wav=38;2;216;180;254:*.flac=38;2;216;180;254:*.ogg=38;2;216;180;254:*.m4a=38;2;216;180;254:*.mp4=38;2;216;180;254:*.mkv=38;2;216;180;254:*.webm=38;2;216;180;254:*.mov=38;2;216;180;254:*.pdf=38;2;199;199;199:*.doc=38;2;199;199;199:*.docx=38;2;199;199;199:*.odt=38;2;199;199;199:*.xls=38;2;199;199;199:*.xlsx=38;2;199;199;199:*.ppt=38;2;199;199;199:*.pptx=38;2;199;199;199:*.txt=38;2;199;199;199:*.cfg=38;2;199;199;199:*.conf=38;2;199;199;199:*.json=38;2;199;199;199:*.yaml=38;2;199;199;199:*.yml=38;2;199;199;199:*.toml=38;2;199;199;199:*.xml=38;2;199;199;199:*.css=38;2;199;199;199:*.html=38;2;199;199;199:*.c=38;2;199;199;199:*.h=38;2;199;199;199:*.cpp=38;2;199;199;199:*.hpp=38;2;199;199;199:*.swp=38;2;199;199;199:*.swo=38;2;199;199;199:*.bak=38;2;199;199;199:*.orig=38;2;199;199;199:*.md=#A5B4FC' export EZA_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:uw=38;2;251;191;36:ux=38;2;251;133;133:gwx=38;2;251;191;36:*.md=#A5B4FC' diff --git a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-light.sh b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-light.sh index 9e6313c5..15cf0ad1 100644 --- a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-light.sh +++ b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-light.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash set -euo pipefail # Dreamcoder Light — LS_COLORS / eza -export LS_COLORS='di=38;2;130;79;22:ex=38;2;167;71;28:ln=38;2;13;74;104:or=48;2;101;67;0;38;2;23;18;13:so=38;2;49;91;49:pi=38;2;101;67;0:bd=38;2;132;47;36:cd=38;2;132;47;36:su=48;2;243;234;220;38;2;167;71;28:sg=48;2;243;234;220;38;2;167;71;28:tw=38;2;130;79;22;48;2;255;247;234:ow=38;2;130;79;22;48;2;255;247;234:st=38;2;130;79;22;48;2;222;203;177:*.tar=38;2;87;71;139:*.tgz=38;2;87;71;139:*.gz=38;2;87;71;139:*.bz2=38;2;87;71;139:*.xz=38;2;87;71;139:*.zst=38;2;87;71;139:*.zip=38;2;87;71;139:*.7z=38;2;87;71;139:*.rar=38;2;87;71;139:*.iso=38;2;87;71;139:*.dmg=38;2;87;71;139:*.jpg=38;2;125;62;100:*.jpeg=38;2;125;62;100:*.png=38;2;125;62;100:*.gif=38;2;125;62;100:*.bmp=38;2;125;62;100:*.svg=38;2;125;62;100:*.webp=38;2;125;62;100:*.ico=38;2;125;62;100:*.mp3=38;2;125;62;100:*.wav=38;2;125;62;100:*.flac=38;2;125;62;100:*.ogg=38;2;125;62;100:*.m4a=38;2;125;62;100:*.mp4=38;2;125;62;100:*.mkv=38;2;125;62;100:*.webm=38;2;125;62;100:*.mov=38;2;125;62;100:*.pdf=38;2;53;46;34:*.doc=38;2;53;46;34:*.docx=38;2;53;46;34:*.odt=38;2;53;46;34:*.xls=38;2;53;46;34:*.xlsx=38;2;53;46;34:*.ppt=38;2;53;46;34:*.pptx=38;2;53;46;34:*.txt=38;2;53;46;34:*.cfg=38;2;53;46;34:*.conf=38;2;53;46;34:*.json=38;2;53;46;34:*.yaml=38;2;53;46;34:*.yml=38;2;53;46;34:*.toml=38;2;53;46;34:*.xml=38;2;53;46;34:*.css=38;2;53;46;34:*.html=38;2;53;46;34:*.c=38;2;53;46;34:*.h=38;2;53;46;34:*.cpp=38;2;53;46;34:*.hpp=38;2;53;46;34:*.swp=38;2;53;46;34:*.swo=38;2;53;46;34:*.bak=38;2;53;46;34:*.orig=38;2;53;46;34:*.sh=38;2;167;71;28:*.bash=38;2;167;71;28:*.zsh=38;2;167;71;28:*.fish=38;2;167;71;28:*.py=38;2;167;71;28:*.rb=38;2;167;71;28:*.rs=38;2;167;71;28:*.go=38;2;167;71;28:*.ts=38;2;167;71;28:*.js=38;2;167;71;28:*.md=#824f16' +export LS_COLORS='di=38;2;130;79;22:ex=38;2;167;71;28:ln=38;2;13;74;104:or=48;2;101;67;0;38;2;243;234;220:so=38;2;49;91;49:pi=38;2;101;67;0:bd=38;2;132;47;36:cd=38;2;132;47;36:su=48;2;243;234;220;38;2;167;71;28:sg=48;2;243;234;220;38;2;167;71;28:tw=38;2;130;79;22;48;2;255;247;234:ow=38;2;130;79;22;48;2;255;247;234:st=38;2;130;79;22;48;2;255;247;234:*.tar=38;2;87;71;139:*.tgz=38;2;87;71;139:*.gz=38;2;87;71;139:*.bz2=38;2;87;71;139:*.xz=38;2;87;71;139:*.zst=38;2;87;71;139:*.zip=38;2;87;71;139:*.7z=38;2;87;71;139:*.rar=38;2;87;71;139:*.iso=38;2;87;71;139:*.dmg=38;2;87;71;139:*.jpg=38;2;125;62;100:*.jpeg=38;2;125;62;100:*.png=38;2;125;62;100:*.gif=38;2;125;62;100:*.bmp=38;2;125;62;100:*.svg=38;2;125;62;100:*.webp=38;2;125;62;100:*.ico=38;2;125;62;100:*.mp3=38;2;125;62;100:*.wav=38;2;125;62;100:*.flac=38;2;125;62;100:*.ogg=38;2;125;62;100:*.m4a=38;2;125;62;100:*.mp4=38;2;125;62;100:*.mkv=38;2;125;62;100:*.webm=38;2;125;62;100:*.mov=38;2;125;62;100:*.pdf=38;2;53;46;34:*.doc=38;2;53;46;34:*.docx=38;2;53;46;34:*.odt=38;2;53;46;34:*.xls=38;2;53;46;34:*.xlsx=38;2;53;46;34:*.ppt=38;2;53;46;34:*.pptx=38;2;53;46;34:*.txt=38;2;53;46;34:*.cfg=38;2;53;46;34:*.conf=38;2;53;46;34:*.json=38;2;53;46;34:*.yaml=38;2;53;46;34:*.yml=38;2;53;46;34:*.toml=38;2;53;46;34:*.xml=38;2;53;46;34:*.css=38;2;53;46;34:*.html=38;2;53;46;34:*.c=38;2;53;46;34:*.h=38;2;53;46;34:*.cpp=38;2;53;46;34:*.hpp=38;2;53;46;34:*.swp=38;2;53;46;34:*.swo=38;2;53;46;34:*.bak=38;2;53;46;34:*.orig=38;2;53;46;34:*.sh=38;2;167;71;28:*.bash=38;2;167;71;28:*.zsh=38;2;167;71;28:*.fish=38;2;167;71;28:*.py=38;2;167;71;28:*.rb=38;2;167;71;28:*.rs=38;2;167;71;28:*.go=38;2;167;71;28:*.ts=38;2;167;71;28:*.js=38;2;167;71;28:*.md=#824f16' export EZA_COLORS='di=38;2;130;79;22:ex=38;2;167;71;28:ln=38;2;13;74;104:so=38;2;49;91;49:pi=38;2;101;67;0:bd=38;2;132;47;36:cd=38;2;132;47;36:uw=38;2;101;67;0:ux=38;2;132;47;36:gwx=38;2;101;67;0:*.md=#824f16' diff --git a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh index 98ad6af4..8a5701dc 100644 --- a/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh +++ b/DreamcoderThemes/dreamcoder/ls-colors-dreamcoder-night.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash set -euo pipefail # 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;198;198;198: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;11;11;13:*.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 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/ls-colors-dreamcoder.sh b/DreamcoderThemes/ls-colors-dreamcoder.sh index 32d5d3e9..948d96d8 100644 --- a/DreamcoderThemes/ls-colors-dreamcoder.sh +++ b/DreamcoderThemes/ls-colors-dreamcoder.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash set -euo pipefail # Dreamcoder Dark — LS_COLORS / eza -export LS_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:or=48;2;251;191;36;38;2;230;230;230:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:su=48;2;0;0;0;38;2;212;181;253:sg=48;2;0;0;0;38;2;212;181;253:tw=38;2;165;180;252;48;2;11;11;11:ow=38;2;165;180;252;48;2;11;11;11:st=38;2;165;180;252;48;2;13;13;15:*.sh=38;2;212;181;253:*.bash=38;2;212;181;253:*.zsh=38;2;212;181;253:*.fish=38;2;212;181;253:*.py=38;2;212;181;253:*.rb=38;2;212;181;253:*.rs=38;2;212;181;253:*.go=38;2;212;181;253:*.ts=38;2;212;181;253:*.js=38;2;212;181;253:*.jpg=38;2;216;180;254:*.jpeg=38;2;216;180;254:*.png=38;2;216;180;254:*.gif=38;2;216;180;254:*.bmp=38;2;216;180;254:*.svg=38;2;216;180;254:*.webp=38;2;216;180;254:*.ico=38;2;216;180;254:*.mp3=38;2;216;180;254:*.wav=38;2;216;180;254:*.flac=38;2;216;180;254:*.ogg=38;2;216;180;254:*.m4a=38;2;216;180;254:*.mp4=38;2;216;180;254:*.mkv=38;2;216;180;254:*.webm=38;2;216;180;254:*.mov=38;2;216;180;254:*.pdf=38;2;199;199;199:*.doc=38;2;199;199;199:*.docx=38;2;199;199;199:*.odt=38;2;199;199;199:*.xls=38;2;199;199;199:*.xlsx=38;2;199;199;199:*.ppt=38;2;199;199;199:*.pptx=38;2;199;199;199:*.txt=38;2;199;199;199:*.cfg=38;2;199;199;199:*.conf=38;2;199;199;199:*.json=38;2;199;199;199:*.yaml=38;2;199;199;199:*.yml=38;2;199;199;199:*.toml=38;2;199;199;199:*.xml=38;2;199;199;199:*.css=38;2;199;199;199:*.html=38;2;199;199;199:*.c=38;2;199;199;199:*.h=38;2;199;199;199:*.cpp=38;2;199;199;199:*.hpp=38;2;199;199;199:*.swp=38;2;199;199;199:*.swo=38;2;199;199;199:*.bak=38;2;199;199;199:*.orig=38;2;199;199;199:*.md=#A5B4FC' +export LS_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:or=48;2;251;191;36;38;2;0;0;0:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:su=48;2;0;0;0;38;2;212;181;253:sg=48;2;0;0;0;38;2;212;181;253:tw=38;2;165;180;252;48;2;11;11;11:ow=38;2;165;180;252;48;2;11;11;11:st=38;2;165;180;252;48;2;11;11;11:*.sh=38;2;212;181;253:*.bash=38;2;212;181;253:*.zsh=38;2;212;181;253:*.fish=38;2;212;181;253:*.py=38;2;212;181;253:*.rb=38;2;212;181;253:*.rs=38;2;212;181;253:*.go=38;2;212;181;253:*.ts=38;2;212;181;253:*.js=38;2;212;181;253:*.jpg=38;2;216;180;254:*.jpeg=38;2;216;180;254:*.png=38;2;216;180;254:*.gif=38;2;216;180;254:*.bmp=38;2;216;180;254:*.svg=38;2;216;180;254:*.webp=38;2;216;180;254:*.ico=38;2;216;180;254:*.mp3=38;2;216;180;254:*.wav=38;2;216;180;254:*.flac=38;2;216;180;254:*.ogg=38;2;216;180;254:*.m4a=38;2;216;180;254:*.mp4=38;2;216;180;254:*.mkv=38;2;216;180;254:*.webm=38;2;216;180;254:*.mov=38;2;216;180;254:*.pdf=38;2;199;199;199:*.doc=38;2;199;199;199:*.docx=38;2;199;199;199:*.odt=38;2;199;199;199:*.xls=38;2;199;199;199:*.xlsx=38;2;199;199;199:*.ppt=38;2;199;199;199:*.pptx=38;2;199;199;199:*.txt=38;2;199;199;199:*.cfg=38;2;199;199;199:*.conf=38;2;199;199;199:*.json=38;2;199;199;199:*.yaml=38;2;199;199;199:*.yml=38;2;199;199;199:*.toml=38;2;199;199;199:*.xml=38;2;199;199;199:*.css=38;2;199;199;199:*.html=38;2;199;199;199:*.c=38;2;199;199;199:*.h=38;2;199;199;199:*.cpp=38;2;199;199;199:*.hpp=38;2;199;199;199:*.swp=38;2;199;199;199:*.swo=38;2;199;199;199:*.bak=38;2;199;199;199:*.orig=38;2;199;199;199:*.md=#A5B4FC' export EZA_COLORS='di=38;2;165;180;252:ex=38;2;212;181;253:ln=38;2;125;211;252:so=38;2;52;211;153:pi=38;2;251;191;36:bd=38;2;251;133;133:cd=38;2;251;133;133:uw=38;2;251;191;36:ux=38;2;251;133;133:gwx=38;2;251;191;36:*.md=#A5B4FC' diff --git a/src/dreamcoder_theme/renderers_extra_shell.py b/src/dreamcoder_theme/renderers_extra_shell.py index db321ccf..df5dd5a1 100644 --- a/src/dreamcoder_theme/renderers_extra_shell.py +++ b/src/dreamcoder_theme/renderers_extra_shell.py @@ -83,7 +83,8 @@ def g(key: str) -> str: "di": _fg(g("accent")), "ex": _fg(g("accent_2")), "ln": _fg(g("diagnostic")), - "or": f"{_bg(c['warning'])};{_fg(g('text'))}", + # Broken links invert onto the warning swatch: base bg text keeps AA. + "or": f"{_bg(c['warning'])};{_fg(bg)}", "so": _fg(g("sage")), "pi": _fg(g("warning")), "bd": _fg(g("error")), @@ -92,7 +93,7 @@ def g(key: str) -> str: "sg": f"{_bg(bg)};{_fg(g('accent_2'))}", "tw": f"{_fg(g('accent'))};{_bg(c['surface0'])}", "ow": f"{_fg(g('accent'))};{_bg(c['surface0'])}", - "st": f"{_fg(g('accent'))};{_bg(c['surface1'])}", + "st": f"{_fg(g('accent'))};{_bg(c['surface0'])}", } groups = { _fg(g("lavender")): [ diff --git a/tests/test_ls_colors_contrast.py b/tests/test_ls_colors_contrast.py new file mode 100644 index 00000000..91627cd6 --- /dev/null +++ b/tests/test_ls_colors_contrast.py @@ -0,0 +1,55 @@ +"""Every coloured LS_COLORS entry must be readable in Dreamcoder Light and Dark. + +Broken symlinks (``or``) and sticky directories (``st``) used colour pairs below +WCAG AA, which made them unreadable in the terminal listing. +""" + +from __future__ import annotations + +import re + +import pytest + +from dreamcoder_theme.palette import load_variants +from dreamcoder_theme.palette_tokens import VARIANTS as DEFAULT_VARIANTS +from dreamcoder_theme.renderers_extra_shell import ls_colors_content +from dreamcoder_theme.settings import theme_paths + +AA = 4.5 + + +def _luminance(hex_rgb: str) -> float: + channels = [int(hex_rgb[i : i + 2], 16) / 255 for i in (0, 2, 4)] + linear = [c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 for c in channels] + return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2] + + +def _contrast(a: str, b: str) -> float: + hi, lo = sorted((_luminance(a), _luminance(b)), reverse=True) + return (hi + 0.05) / (lo + 0.05) + + +def _truecolor(params: list[str], selector: str) -> str | None: + for i in range(len(params) - 4): + if params[i] == selector and params[i + 1] == "2": + return "".join(f"{int(v):02x}" for v in params[i + 2 : i + 5]) + return None + + +@pytest.mark.parametrize("mode", ["light", "dark"]) +def test_ls_colors_entries_meet_aa(mode): + palette = load_variants(DEFAULT_VARIANTS, theme_paths().tokens_file)[mode] + snippet = ls_colors_content(palette) + ls_colors = re.search(r"export LS_COLORS='([^']+)'", snippet).group(1) + base_bg = palette["bg"].lstrip("#").lower() + failures = [] + for entry in ls_colors.split(":"): + key, _, value = entry.partition("=") + params = value.split(";") + fg = _truecolor(params, "38") + if fg is None: + continue + ratio = _contrast(fg, _truecolor(params, "48") or base_bg) + if ratio < AA: + failures.append(f"{key}={ratio:.2f}") + assert not failures, f"{mode}: entries below {AA}:1 -> {failures}" From b2e571045e99ef115d8b3d8f1476e048129085a4 Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 14:02:45 -0500 Subject: [PATCH 6/7] fix(ml4w): restore dreamcoder colours after ml4w's gtk listener runs matugen ML4W 2.16's gtk-theme-switcher listener re-runs Matugen over the wallpaper on every gtk settings.ini change, including Dreamcoder's own mode switch, and overwrites Dreamcoder colour files before reloading Quickshell, Waybar, GTK and swaync. apply-ml4w-hooks.sh now inserts a marked block after each Matugen call that runs dreamcoder sync synchronously in the branch's mode (repo writes off, logged, bounded), and restarts the listener through listeners.sh when the file changes. Records T6 evidence and T8 in the feature document. --- docs/configuration/ml4w.md | 25 +++- odd/tasks/ml4w-2.16-gentleman-sync.md | 48 ++++++- scripts/apply-ml4w-hooks.sh | 105 +++++++++++++- tests/fixtures/ml4w/gtk-theme-switcher-2.16 | 94 +++++++++++++ tests/ml4w/apply_ml4w_hooks.bats | 144 ++++++++++++++++++++ 5 files changed, 410 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/ml4w/gtk-theme-switcher-2.16 diff --git a/docs/configuration/ml4w.md b/docs/configuration/ml4w.md index ae3fb566..e17f97d5 100644 --- a/docs/configuration/ml4w.md +++ b/docs/configuration/ml4w.md @@ -131,6 +131,27 @@ one of those files silently disappears on the next upgrade. The rule: wallpaper app calls with `$IMAGE_PATH`) between `# >>> Dreamcoder wallpaper hook >>>` markers. Each run replaces the block, so re-running it after an ML4W upgrade restores the hook without duplicates. +- **The GTK theme listener is hooked the same way.** ML4W 2.16 runs + `~/.config/ml4w/listeners/gtk-theme-switcher.sh`, which watches + `~/.config/gtk-3.0/settings.ini`. Dreamcoder's own light/dark switch writes + that file, so the listener then runs Matugen over the wallpaper and + overwrites Dreamcoder's colour files (`waybar/colors.css`, `hypr/colors.lua`, + `hypr/colors.conf`, rofi colours) before reloading Quickshell, Waybar, GTK and + swaync. `scripts/apply-ml4w-hooks.sh` inserts a block between + `# >>> Dreamcoder listener hook >>>` markers right after each Matugen call. + The block runs `scripts/dreamcoder sync` synchronously + (`DREAMCODER_THEME_MODE` taken from the branch's `matugen -m dark|light`, or + read from `settings.ini` when the call has no `-m`; `DREAMCODER_WRITE_REPO=0`; + streams to + `~/.cache/dreamcoder/ml4w-listener-sync.log`, 120 s timeout), so the reloads + that follow pick up Dreamcoder colours. The sync never writes `settings.ini`, + so the listener cannot loop. When the file changes, the running listener is + 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. - **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 @@ -144,7 +165,7 @@ After every ML4W upgrade: ```bash ./scripts/generate-custom-lua.sh # custom.lua (keybinds + colour loader) -./scripts/apply-ml4w-hooks.sh # re-hook the wallpaper runner +./scripts/apply-ml4w-hooks.sh # re-hook the wallpaper runner + GTK listener ./scripts/dreamcoder sync # rewrite colors.lua / colors.conf ./scripts/verify-ml4w-setup.sh ``` @@ -200,7 +221,7 @@ DreamcoderProfiles/dreamcoder/ └── asus-vivobook15.json # ASUS VivoBook 15 profile (all Fn keys) tests/ml4w/ -├── apply_ml4w_hooks.bats # wallpaper hook (ML4W 2.16 runner fixture) +├── apply_ml4w_hooks.bats # wallpaper + GTK listener hooks (ML4W 2.16 fixtures) ├── args.bats # script argument handling ├── generate_custom_lua.bats # generator, incl. the dreamcoder-colors loader ├── keybindings_variant.bats # dreamcoder.lua vs ML4W 2.16 and the profile diff --git a/odd/tasks/ml4w-2.16-gentleman-sync.md b/odd/tasks/ml4w-2.16-gentleman-sync.md index 798b3b10..380e2d20 100644 --- a/odd/tasks/ml4w-2.16-gentleman-sync.md +++ b/odd/tasks/ml4w-2.16-gentleman-sync.md @@ -120,8 +120,23 @@ injected into ML4W-owned files. systemd unit links (absolute repo links), `.config/systemd/user/dreamcoder-env.conf` (identical regular file), warp settings/theme links. ML4W's `fish_variables` (universal vars incl. `fish_user_paths`) is not migrated — report to the user. -- [ ] T6 — Relink the live system through `dreamcoder repair`, confirm a new +- [x] T6 — Relink the live system through `dreamcoder repair`, confirm a new interactive fish starts Herdr. Route: inline. + - Evidence: `./scripts/dreamcoder repair` (backup id `20260928-133204-402020`, + conflicts moved to + `~/.local/share/dreamcoder/install-conflicts/20260928-133204-402020/`) relinked + fish, kitty, fastfetch, `.bashrc`, `.zshrc` and the other Dreamcoder stow + targets; fish loads the Dreamcoder config; an interactive fish in a pty + autostarted the Herdr 0.9.1 server. + - Fixes found while relinking: `a7c925a` (Herdr panes spawned zsh: `SHELL` + pinned to fish), `702751f` (`restart_waybar` leaked the caller's stdout, so the + theme apply hung), `a107ff0` (ML4W reset btop `color_theme` to matugen; the + Dreamcoder theme is re-selected on every apply). + - Not migrated: ML4W's `fish_variables` (universal vars); the only effective + difference is `/usr/lib/ccache/bin` missing from `PATH`. + - Found: during `repair` (13:33:12) ML4W's GTK theme listener re-ran Matugen and + overwrote `waybar/colors.css` and `hypr/colors.lua`; `verify-ml4w-setup.sh` + failed until `dreamcoder sync` — addressed by T8. - [x] T7 — Herdr 0.9.1: live binary updated with `herdr update` (0.9.0 → 0.9.1), integrations reinstalled, `herdr config check` ok. Onboard a `herdr-0.9.1` profile per `docs/herdr.md` (installed-binary evidence, generated variants, verify-repo-sync). @@ -142,6 +157,35 @@ injected into ML4W-owned files. `pane run` from inside a Herdr pane (`HERDR_ENV`). - Not observed: live `server reload-config` (server not running at capture). +- [x] T8 — Hook ML4W 2.16's GTK theme listener + (`~/.config/ml4w/listeners/gtk-theme-switcher.sh`): on every gtk `settings.ini` + change (every Dreamcoder mode switch, including the 07:00/16:00/18:00 timer) it + runs Matugen over the wallpaper and overwrites Dreamcoder colour files before + reloading Quickshell, Waybar, GTK and swaync. `scripts/apply-ml4w-hooks.sh` now + inserts a marked block after each Matugen call that runs `dreamcoder sync` + synchronously, and restarts the listener through ML4W's `listeners.sh` when the + file changes. Route: delegated writer. + - Evidence: commit `fix(ml4w): restore Dreamcoder colours after ML4W's GTK + listener runs Matugen`; `bats tests/ml4w/ tests/shell/` 143/144 (only the known + kanagawa failure); `tests/ml4w/apply_ml4w_hooks.bats` 25/25 (10 new listener + tests on a read-only copy of the installed 2.16 listener); `python -m pytest + tests/` 694 passed; `bash -n` + shellcheck clean. + - Rationale: same philosophy as the runner hook (marked, byte-identical on + re-run, stale blocks replaced, write-through keeps symlink/mode, missing + listener skipped, no Matugen call → untouched with a warning, overrides + `ML4W_GTK_LISTENER` / `ML4W_LISTENERS_SCRIPT`). The block passes the branch's + own `matugen -m dark|light` as `DREAMCODER_THEME_MODE` (fallback: read + `settings.ini`), because `sync` defaulted to Dark without it; it sets + `DREAMCODER_WRITE_REPO=0` so a background listener never dirties the repo, + logs to `~/.cache/dreamcoder/ml4w-listener-sync.log`, and is bounded by a + 120 s timeout. `sync` writes no gtk `settings.ini`, so the listener cannot + loop. The restart runs in the foreground with detached streams so the + `theme-auto.sh` apply that follows never races a half-restarted listener. + - Known gap: with the Night render profile persisted, `sync` only regenerates + repository artifacts, so live Night colours are not restored by the block. + - Pending: live apply (hook the live listener, toggle dark/light, verify) — + run by the parent after the persisted-mode fix. + ## Acceptance criteria - `bats tests/ml4w/` and `python -m pytest tests/` pass. @@ -157,5 +201,5 @@ injected into ML4W-owned files. ## Next step -T6 (inline live relink). Pending user decisions: SUPER+SHIFT+arrows +T8 live apply (parent). Pending user decisions: SUPER+SHIFT+arrows overlap (resize vs move), the pre-existing kanagawa bridge test, push/PR of the branch. diff --git a/scripts/apply-ml4w-hooks.sh b/scripts/apply-ml4w-hooks.sh index ec2bed22..268b451a 100755 --- a/scripts/apply-ml4w-hooks.sh +++ b/scripts/apply-ml4w-hooks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # ============================================================================ -# apply-ml4w-hooks.sh — (re)install Dreamcoder's wallpaper hook into ML4W +# apply-ml4w-hooks.sh — (re)install Dreamcoder's hooks into ML4W # ============================================================================ # Primary target: ML4W's wallpaper runner (~/.config/ml4w/scripts/ml4w-wallpaper, # which receives the image as $IMAGE_PATH). ML4W 2.16 drives it directly from @@ -12,8 +12,20 @@ # never duplicated; the unmarked block older releases of this script appended # is migrated to the marked form. # +# Second target: ML4W's GTK theme listener +# (~/.config/ml4w/listeners/gtk-theme-switcher.sh). It watches +# ~/.config/gtk-3.0/settings.ini and, on every change (including Dreamcoder's +# own light/dark switch), runs Matugen over the wallpaper, which overwrites +# Dreamcoder's colour files, then reloads Quickshell, Waybar, GTK and swaync. +# A marked block after each Matugen call re-applies Dreamcoder colours +# synchronously through `dreamcoder sync`, so those reloads pick up Dreamcoder +# colours. The sync never writes settings.ini, so the listener cannot loop. +# The running listener already parsed the old body, so it is restarted through +# ML4W's listeners.sh whenever the file changes. +# # Overrides: ML4W_WALLPAPER_SCRIPT, ML4W_WALLPAPER_VAR (older ML4W releases used -# ~/.config/hypr/scripts/wallpaper.sh with $used_wallpaper), WAYPAPER_CONFIG. +# ~/.config/hypr/scripts/wallpaper.sh with $used_wallpaper), WAYPAPER_CONFIG, +# ML4W_GTK_LISTENER, ML4W_LISTENERS_SCRIPT. # ============================================================================ set -euo pipefail @@ -26,6 +38,10 @@ HOOK_SCRIPT="${DREAMCODER_DOTS_DIR}/scripts/wallpaper-hook.sh" BEGIN_MARK='# >>> Dreamcoder wallpaper hook >>>' END_MARK='# <<< Dreamcoder wallpaper hook <<<' LEGACY_MARK='# Dreamcoder final wallpaper/theme sync' +ML4W_GTK_LISTENER="${ML4W_GTK_LISTENER:-${HOME}/.config/ml4w/listeners/gtk-theme-switcher.sh}" +ML4W_LISTENERS_SCRIPT="${ML4W_LISTENERS_SCRIPT:-${HOME}/.config/ml4w/listeners.sh}" +LISTENER_BEGIN_MARK='# >>> Dreamcoder listener hook >>>' +LISTENER_END_MARK='# <<< Dreamcoder listener hook <<<' # Print the runner without any Dreamcoder block (marked or legacy) and without # trailing blank lines, so re-appending always yields the same bytes. @@ -69,6 +85,90 @@ ${END_MARK}" printf '✓ ML4W wallpaper runner hooked: %s\n' "${ML4W_WALLPAPER_SCRIPT}" } +# Print the listener with a fresh Dreamcoder block after every Matugen call +# (existing blocks are dropped first, so re-running yields the same bytes). +# The sync mode is the branch's own `-m dark|light`; a Matugen call without +# one falls back to reading gtk-application-prefer-dark-theme at run time, +# because `dreamcoder sync` renders Dark when DREAMCODER_THEME_MODE is unset. +# Exits 3 when no Matugen call is found, leaving the decision to the caller. +render_listener_hook() { + awk -v begin="${LISTENER_BEGIN_MARK}" -v end="${LISTENER_END_MARK}" \ + -v dispatcher="${DREAMCODER_DOTS_DIR}/scripts/dreamcoder" ' + function trim(s) { sub(/^[[:space:]]+/, "", s); sub(/[[:space:]]+$/, "", s); return s } + trim($0) == begin { skip = 1; next } + skip && trim($0) == end { skip = 0; next } + skip { next } + { + print + if ($0 ~ /^[[:space:]]*[^#]*(\$MATUGEN_BIN|\$\{MATUGEN_BIN\}|(^|[[:space:]])matugen)[[:space:]]+image([[:space:]]|$)/) { + found++ + match($0, /^[[:space:]]*/) + pad = substr($0, 1, RLENGTH) + print pad begin + print pad "# Managed by dreamcoder-dots scripts/apply-ml4w-hooks.sh; re-run it after ML4W upgrades." + print pad "# Matugen just rewrote the colour files from the wallpaper: restore Dreamcoder" + print pad "# colours before the reloads below. The sync never writes gtk settings.ini." + print pad "if [[ -x \"" dispatcher "\" ]]; then" + if (match($0, /-m[[:space:]]+"?(dark|light)"?/)) { + # The branch passes its mode to Matugen: reuse it verbatim. + mode = substr($0, RSTART, RLENGTH) + sub(/^-m[[:space:]]+"?/, "", mode) + sub(/"$/, "", mode) + print pad " _dreamcoder_mode=" mode + } else { + print pad " _dreamcoder_mode=light" + print pad " grep -Eq \"^gtk-application-prefer-dark-theme=(1|true)$\" \"${SETTINGS_FILE:-$HOME/.config/gtk-3.0/settings.ini}\" && _dreamcoder_mode=dark" + } + print pad " mkdir -p \"$HOME/.cache/dreamcoder\"" + print pad " DREAMCODER_THEME_MODE=\"$_dreamcoder_mode\" DREAMCODER_WRITE_REPO=0 timeout 120 \"" dispatcher "\" sync \"$HOME/.cache/dreamcoder/ml4w-listener-sync.log\" 2>&1 || true" + print pad "fi" + print pad end + } + } + END { if (!found) exit 3 } + ' "$1" +} + +hook_gtk_listener() { + if [[ ! -f "${ML4W_GTK_LISTENER}" ]]; then + printf '⚠ ML4W GTK theme listener not found, skipped: %s\n' "${ML4W_GTK_LISTENER}" >&2 + return 0 + fi + local current desired rc=0 + current="$(cat "${ML4W_GTK_LISTENER}")" + desired="$(render_listener_hook "${ML4W_GTK_LISTENER}")" || rc=$? + if [[ "${rc}" -eq 3 ]]; then + printf '⚠ no Matugen call found in the ML4W GTK theme listener, left untouched: %s\n' "${ML4W_GTK_LISTENER}" >&2 + return 0 + elif [[ "${rc}" -ne 0 ]]; then + return "${rc}" + fi + if [[ "${current}" == "${desired}" ]]; then + printf '✓ ML4W GTK theme listener hook already current\n' + return 0 + fi + # Write through the path, like the runner hook (keeps symlink and mode). + printf '%s\n' "${desired}" >"${ML4W_GTK_LISTENER}" + printf '✓ ML4W GTK theme listener hooked: %s\n' "${ML4W_GTK_LISTENER}" + restart_gtk_listener +} + +# The live listener keeps the body it parsed at start, so restart it through +# ML4W's own control script. Runs in the foreground (listeners.sh returns after +# ~1s) so the theme apply below never races a half-restarted listener; every +# stream is detached so the relaunched listener cannot hold the caller's pipes. +restart_gtk_listener() { + if [[ ! -x "${ML4W_LISTENERS_SCRIPT}" ]]; then + printf '⚠ ML4W listeners.sh not found, restart the GTK theme listener manually: %s\n' "${ML4W_LISTENERS_SCRIPT}" >&2 + return 0 + fi + if timeout 30 "${ML4W_LISTENERS_SCRIPT}" --restart gtk-theme-switcher /dev/null 2>&1; then + printf '✓ ML4W GTK theme listener restarted\n' + else + printf '⚠ ML4W GTK theme listener restart failed; run: %s --restart gtk-theme-switcher\n' "${ML4W_LISTENERS_SCRIPT}" >&2 + fi +} + hook_waypaper() { if [[ ! -f "${WAYPAPER_CONFIG}" ]]; then printf '✓ waypaper config absent (not used by ML4W 2.16), skipped\n' @@ -87,6 +187,7 @@ hook_waypaper() { } hook_ml4w_runner +hook_gtk_listener hook_waypaper "${DREAMCODER_DOTS_DIR}/scripts/theme-auto.sh" diff --git a/tests/fixtures/ml4w/gtk-theme-switcher-2.16 b/tests/fixtures/ml4w/gtk-theme-switcher-2.16 new file mode 100644 index 00000000..1ae8dbe5 --- /dev/null +++ b/tests/fixtures/ml4w/gtk-theme-switcher-2.16 @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# This script monitors changes to the GTK settings.ini file +# and automatically switches the 'matugen' theme between light and dark +# based on the 'gtk-application-prefer-dark-theme' setting. + +# Path to the GTK settings file +SETTINGS_FILE="$HOME/.config/gtk-3.0/settings.ini" +SETTINGS_DIR="$HOME/.config/gtk-3.0" +SETTINGS_BASENAME=$(basename "$SETTINGS_FILE") + +# Ensure inotify-tools is installed +if ! command -v inotifywait &> /dev/null +then + echo "Error: inotifywait is not installed." + echo "Please install inotify-tools (e.g., sudo apt install inotify-tools on Debian/Ubuntu)" + exit 1 +fi + +echo "Monitoring $SETTINGS_FILE for changes..." +echo "Press Ctrl+C to stop." + +# Function to apply the theme based on the current settings +apply_theme() { + # Check if the settings file exists + if [ ! -f "$SETTINGS_FILE" ]; then + echo "Error: $SETTINGS_FILE not found. Please ensure the file exists." + return 1 + fi + + # Determine matugen binary path + if [ -f $HOME/.cargo/bin/matugen ]; then + MATUGEN_BIN="$HOME/.cargo/bin/matugen" + elif [ -f $HOME/.local/bin/matugen ]; then + MATUGEN_BIN="$HOME/.local/bin/matugen" + else + MATUGEN_BIN="matugen" + fi + + # Extract the value of gtk-application-prefer-dark-theme + # We use grep to find the line and awk to get the value after the '=' + THEME_PREF=$(grep -E '^gtk-application-prefer-dark-theme=' "$SETTINGS_FILE" | awk -F'=' '{print $2}') + + if [ -z "$THEME_PREF" ]; then + echo "Warning: 'gtk-application-prefer-dark-theme' setting not found in $SETTINGS_FILE. Skipping theme application." + return 0 + fi + + if [[ "$THEME_PREF" == "1" || "$THEME_PREF" == "true" ]]; then + echo "Detected dark theme preference (gtk-application-prefer-dark-theme=1/true). Applying dark matugen theme..." + $MATUGEN_BIN image $(cat ~/.cache/ml4w/hyprland-dotfiles/current_wallpaper) --source-color-index 0 -m "dark" + + # Update Quickshell theme + qs ipc call theme-manager reload + echo "Quickshell Theme updated" + + # Update ML4W Dotfiles Settings theme + qs -p $HOME/.local/share/ml4w-dotfiles-settings/quickshell ipc call theme-manager reload + echo "ML4W Dotfiles Settings Theme updated" + + # Reload Waybar + nohup bash -c "$HOME/.config/waybar/launch.sh" > /dev/null 2>&1 & + disown + + $HOME/.config/hypr/scripts/gtk.sh & + + swaync-client -rs + elif [[ "$THEME_PREF" == "0" || "$THEME_PREF" == "false" ]]; then + echo "Detected light theme preference (gtk-application-prefer-dark-theme=0/false). Applying light matugen theme..." + $MATUGEN_BIN image $(cat ~/.cache/ml4w/hyprland-dotfiles/current_wallpaper) --source-color-index 0 -m "light" + + # Update Quickshell theme + qs ipc call theme-manager reload + echo "Quickshell Theme updated" + + # Reload Waybar + nohup bash -c "$HOME/.config/waybar/launch.sh" > /dev/null 2>&1 & + disown + + $HOME/.config/hypr/scripts/gtk.sh & + + swaync-client -rs + else + echo "Warning: Unexpected value for gtk-application-prefer-dark-theme: $THEME_PREF. Expected 0/1/true/false. Skipping theme application." + fi +} + +# Loop indefinitely, reading output from inotifywait +inotifywait -m -q -e close_write,moved_to "$SETTINGS_DIR" | while read -r dir events filename; do + if [[ "$filename" == "$SETTINGS_BASENAME" ]]; then + echo "Change detected in $SETTINGS_FILE. Re-applying theme..." + apply_theme + fi +done diff --git a/tests/ml4w/apply_ml4w_hooks.bats b/tests/ml4w/apply_ml4w_hooks.bats index 8e0797f2..2137843d 100644 --- a/tests/ml4w/apply_ml4w_hooks.bats +++ b/tests/ml4w/apply_ml4w_hooks.bats @@ -10,8 +10,14 @@ # The optional waypaper injection goes through `sed`. In a sed replacement, `&` # expands to the whole match, and the injected hook text contains `2>&1`, so an # unescaped `&` re-inserts the matched line and corrupts the config. +# +# The GTK theme listener fixture is a read-only copy of ML4W 2.16's installed +# ~/.config/ml4w/listeners/gtk-theme-switcher.sh. It runs Matugen over the +# wallpaper on every gtk settings.ini change; the hook restores Dreamcoder +# colours right after each Matugen call. RUNNER_FIXTURE="${BATS_TEST_DIRNAME}/../fixtures/ml4w/ml4w-wallpaper-2.16" +LISTENER_FIXTURE="${BATS_TEST_DIRNAME}/../fixtures/ml4w/gtk-theme-switcher-2.16" setup() { TEST_DIR="$(mktemp -d)" @@ -26,6 +32,13 @@ setup() { printf '[Settings]\npost_command = ~/.config/ml4w/scripts/ml4w-wallpaper "$wallpaper" --skip > /dev/null 2>&1\n' >"${WAYPAPER_CONFIG}" cp "${RUNNER_FIXTURE}" "${ML4W_WALLPAPER_SCRIPT}" chmod +x "${ML4W_WALLPAPER_SCRIPT}" + # Exported so no invocation can reach the live ML4W listener or restart it. + export ML4W_GTK_LISTENER="${TEST_DIR}/gtk-theme-switcher.sh" + export ML4W_LISTENERS_SCRIPT="${TEST_DIR}/listeners.sh" + cp "${LISTENER_FIXTURE}" "${ML4W_GTK_LISTENER}" + chmod +x "${ML4W_GTK_LISTENER}" + printf '#!/usr/bin/env bash\necho "$*" >>"%s/restarts"\n' "${TEST_DIR}" >"${ML4W_LISTENERS_SCRIPT}" + chmod +x "${ML4W_LISTENERS_SCRIPT}" } teardown() { @@ -47,6 +60,14 @@ count_blocks() { grep -c '^# >>> Dreamcoder wallpaper hook >>>$' "${ML4W_WALLPAPER_SCRIPT}" || true } +count_listener_blocks() { + grep -c '^[[:space:]]*# >>> Dreamcoder listener hook >>>$' "${ML4W_GTK_LISTENER}" || true +} + +count_restarts() { + if [[ -f "${TEST_DIR}/restarts" ]]; then wc -l <"${TEST_DIR}/restarts" | tr -d ' '; else echo 0; fi +} + # ── ML4W runner (primary) ──────────────────────────────────────────────────── @test "apply-ml4w-hooks: the 2.16 runner gets exactly one marked block" { @@ -170,3 +191,126 @@ LEGACY # The corrupt form repeats the original assignment inside the line. [ "$(count_in_post_command 'ml4w-wallpaper')" -eq 1 ] } + +# ── GTK theme listener (Matugen over Dreamcoder colours) ───────────────────── + +@test "apply-ml4w-hooks: a hook block follows each Matugen call in the listener" { + run run_hooks + [ "$status" -eq 0 ] + [ "$(count_listener_blocks)" -eq 2 ] + local matugen_lines block_lines + matugen_lines="$(grep -n 'MATUGEN_BIN image' "${ML4W_GTK_LISTENER}" | cut -d: -f1 | tr '\n' ' ')" + block_lines="$(grep -n 'Dreamcoder listener hook >>>' "${ML4W_GTK_LISTENER}" | cut -d: -f1 | tr '\n' ' ')" + read -r m1 m2 <<<"${matugen_lines}" + read -r b1 b2 <<<"${block_lines}" + [ "${b1}" -eq $((m1 + 1)) ] + [ "${b2}" -eq $((m2 + 1)) ] + bash -n "${ML4W_GTK_LISTENER}" + [ -x "${ML4W_GTK_LISTENER}" ] +} + +@test "apply-ml4w-hooks: the listener is restarted once when its hook changes" { + run_hooks + [ "$(count_restarts)" -eq 1 ] + grep -qx -- '--restart gtk-theme-switcher' "${TEST_DIR}/restarts" +} + +@test "apply-ml4w-hooks: re-running leaves the listener byte-identical and running" { + run_hooks + cp "${ML4W_GTK_LISTENER}" "${TEST_DIR}/first-listener" + run run_hooks + [ "$status" -eq 0 ] + [[ "$output" == *"listener hook already current"* ]] + cmp -s "${TEST_DIR}/first-listener" "${ML4W_GTK_LISTENER}" + [ "$(count_listener_blocks)" -eq 2 ] + [ "$(count_restarts)" -eq 1 ] +} + +@test "apply-ml4w-hooks: a stale listener block is replaced, not duplicated" { + run_hooks + sed -i 's|timeout 120|timeout 5|' "${ML4W_GTK_LISTENER}" + run_hooks + [ "$(count_listener_blocks)" -eq 2 ] + run grep -q 'timeout 5 ' "${ML4W_GTK_LISTENER}" + [ "$status" -ne 0 ] +} + +@test "apply-ml4w-hooks: a listener without a Matugen call is left untouched" { + grep -v 'MATUGEN_BIN image' "${LISTENER_FIXTURE}" >"${ML4W_GTK_LISTENER}" + cp "${ML4W_GTK_LISTENER}" "${TEST_DIR}/no-matugen" + run run_hooks + [ "$status" -eq 0 ] + [[ "$output" == *"no Matugen call found"* ]] + cmp -s "${TEST_DIR}/no-matugen" "${ML4W_GTK_LISTENER}" + [ "$(count_restarts)" -eq 0 ] +} + +@test "apply-ml4w-hooks: a missing listener is skipped without failing" { + rm -f "${ML4W_GTK_LISTENER}" + run run_hooks + [ "$status" -eq 0 ] + [[ "$output" == *"GTK theme listener not found"* ]] + [ ! -e "${ML4W_GTK_LISTENER}" ] + [ "$(count_blocks)" -eq 1 ] +} + +@test "apply-ml4w-hooks: a symlinked listener stays a symlink" { + mv "${ML4W_GTK_LISTENER}" "${TEST_DIR}/listener-target" + ln -s "${TEST_DIR}/listener-target" "${ML4W_GTK_LISTENER}" + run_hooks + [ -L "${ML4W_GTK_LISTENER}" ] + [ "$(count_listener_blocks)" -eq 2 ] +} + +@test "apply-ml4w-hooks: the listener block runs a colours-only dispatcher sync" { + run_hooks + # Stub dispatcher records its arguments and the environment it gets. + cat >"${TEST_DIR}/scripts/dreamcoder" <>"${TEST_DIR}/dispatched" +STUB + chmod +x "${TEST_DIR}/scripts/dreamcoder" + mkdir -p "${TEST_DIR}/home" + extract_block() { + awk -v want="$1" '/# >>> Dreamcoder listener hook >>>/ { n++; on = (n == want) } on { print } on && /# <<< Dreamcoder listener hook <<"${TEST_DIR}/block-dark.sh" + extract_block 2 >"${TEST_DIR}/block-light.sh" + # The mode comes from each branch's Matugen call, not from settings.ini. + printf '[Settings]\ngtk-application-prefer-dark-theme=0\n' >"${TEST_DIR}/settings.ini" + HOME="${TEST_DIR}/home" SETTINGS_FILE="${TEST_DIR}/settings.ini" bash "${TEST_DIR}/block-dark.sh" + printf '[Settings]\ngtk-application-prefer-dark-theme=1\n' >"${TEST_DIR}/settings.ini" + HOME="${TEST_DIR}/home" SETTINGS_FILE="${TEST_DIR}/settings.ini" bash "${TEST_DIR}/block-light.sh" + [ "$(sed -n 1p "${TEST_DIR}/dispatched")" = "sync|dark|0" ] + [ "$(sed -n 2p "${TEST_DIR}/dispatched")" = "sync|light|0" ] + [ -f "${TEST_DIR}/home/.cache/dreamcoder/ml4w-listener-sync.log" ] + cat "${TEST_DIR}/block-dark.sh" "${TEST_DIR}/block-light.sh" >"${TEST_DIR}/block.sh" + # The block must never write the file the listener watches (no loop). + run grep -E 'settings\.ini"? *>|>>? *"?\$\{?SETTINGS_FILE' "${TEST_DIR}/block.sh" + [ "$status" -ne 0 ] +} + +@test "apply-ml4w-hooks: without listeners.sh the hook is applied with a warning" { + rm -f "${ML4W_LISTENERS_SCRIPT}" + run run_hooks + [ "$status" -eq 0 ] + [[ "$output" == *"listeners.sh not found"* ]] + [ "$(count_listener_blocks)" -eq 2 ] +} + +@test "apply-ml4w-hooks: a Matugen call without -m falls back to settings.ini" { + sed 's| -m "dark"||; s| -m "light"||' "${LISTENER_FIXTURE}" >"${ML4W_GTK_LISTENER}" + run_hooks + [ "$(count_listener_blocks)" -eq 2 ] + printf '#!/usr/bin/env bash\necho "${DREAMCODER_THEME_MODE}" >>"%s/dispatched"\n' "${TEST_DIR}" >"${TEST_DIR}/scripts/dreamcoder" + chmod +x "${TEST_DIR}/scripts/dreamcoder" + mkdir -p "${TEST_DIR}/home" + awk '/# >>> Dreamcoder listener hook >>>/ { on = 1 } on { print } /# <<< Dreamcoder listener hook <<"${TEST_DIR}/block.sh" + printf 'gtk-application-prefer-dark-theme=true\n' >"${TEST_DIR}/settings.ini" + HOME="${TEST_DIR}/home" SETTINGS_FILE="${TEST_DIR}/settings.ini" bash "${TEST_DIR}/block.sh" + printf 'gtk-application-prefer-dark-theme=0\n' >"${TEST_DIR}/settings.ini" + HOME="${TEST_DIR}/home" SETTINGS_FILE="${TEST_DIR}/settings.ini" bash "${TEST_DIR}/block.sh" + [ "$(tr '\n' ' ' <"${TEST_DIR}/dispatched")" = "dark light " ] +} From 12256a69e07500a926ecb9826248a4c1f545bf7b Mon Sep 17 00:00:00 2001 From: DreamCoder08 Date: Mon, 28 Sep 2026 14:05:42 -0500 Subject: [PATCH 7/7] docs(odd): record live evidence for the ml4w listener hook --- odd/tasks/ml4w-2.16-gentleman-sync.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/odd/tasks/ml4w-2.16-gentleman-sync.md b/odd/tasks/ml4w-2.16-gentleman-sync.md index 380e2d20..d2656fa2 100644 --- a/odd/tasks/ml4w-2.16-gentleman-sync.md +++ b/odd/tasks/ml4w-2.16-gentleman-sync.md @@ -203,3 +203,13 @@ injected into ML4W-owned files. T8 live apply (parent). Pending user decisions: SUPER+SHIFT+arrows overlap (resize vs move), the pre-existing kanagawa bridge test, push/PR of the branch. + +## Live evidence (T8, 2026-09-28) + +- `./scripts/apply-ml4w-hooks.sh`: listener hooked (markers present) and restarted. +- Dark → Light toggle through the listener: final waybar/hypr colours carry the + Dreamcoder light palette; `verify-ml4w-setup.sh` 20 passed / 0 failed. +- Also fixed during this pass: `b9f8c50` theme_mode() falls back to the persisted + live mode; `bd8e5b5` fish starts in the persisted mode; `4b04f3e` ls colours `or`/`st` + lifted to WCAG AA with a 4.5:1 gate. +- Follow-up feature: `odd/tasks/remove-night-profile.md` (user decision: Light + Dark only).