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/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/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/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/odd/tasks/ml4w-2.16-gentleman-sync.md b/odd/tasks/ml4w-2.16-gentleman-sync.md index 798b3b10..d2656fa2 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,15 @@ 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. + +## 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). 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/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/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/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/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 " ] +} 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" +} 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" ] +} 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:" ] +} 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}" 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"