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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/configuration/ml4w.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ one of those files silently disappears on the next upgrade. The rule:
(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`.
- **Matugen no longer writes Dreamcoder's files.** Restoring colours after Matugen is a
race: Matugen rewrites `hypr/colors.conf`, `hypr/colors.lua`, `waybar/colors.css`,
`rofi/colors.rasi` and, through a symlink into Waybar, `swaync/colors.css` on every
wallpaper or mode change, and a late write beats the restore. The hook comments out those
five `[templates.*]` sections in `~/.config/matugen/config.toml` with a `#dreamcoder-off# `
prefix (idempotent; the result is validated as TOML before it is written). Every other
template (kitty, btop, GTK, quickshell, ...) keeps running. An ML4W upgrade restores the
stock file, so re-run `./scripts/apply-ml4w-hooks.sh` (or `dreamcoder repair`). To undo by
hand: `sed -i 's/^#dreamcoder-off# //' ~/.config/matugen/config.toml`. Override the path
with `MATUGEN_CONFIG`.
- **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
Expand Down
103 changes: 90 additions & 13 deletions scripts/apply-ml4w-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,22 @@
# ============================================================================
set -euo pipefail

source "${DREAMCODER_DOTS_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/lib/env.sh"
ENV_LIB="${DREAMCODER_DOTS_DIR:-$(cd "$(dirname "$0")/.." && pwd)}/lib/env.sh"
if [[ ! -f "${ENV_LIB}" ]]; then
printf '✗ Required library not found: %s\n' "${ENV_LIB}" >&2
exit 1
fi
# shellcheck source=../lib/env.sh
source "${ENV_LIB}"
ensure_dots_dir
WAYPAPER_CONFIG="${WAYPAPER_CONFIG:-${HOME}/.config/waypaper/config.ini}"
ML4W_WALLPAPER_SCRIPT="${ML4W_WALLPAPER_SCRIPT:-${HOME}/.config/ml4w/scripts/ml4w-wallpaper}"
ML4W_WALLPAPER_VAR="${ML4W_WALLPAPER_VAR:-IMAGE_PATH}"
# It is embedded in the generated runner block as a variable name.
if [[ ! "${ML4W_WALLPAPER_VAR}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
printf '✗ ML4W_WALLPAPER_VAR is not a valid shell identifier: %s\n' "${ML4W_WALLPAPER_VAR}" >&2
exit 1
fi
HOOK_SCRIPT="${DREAMCODER_DOTS_DIR}/scripts/wallpaper-hook.sh"
BEGIN_MARK='# >>> Dreamcoder wallpaper hook >>>'
END_MARK='# <<< Dreamcoder wallpaper hook <<<'
Expand All @@ -42,6 +53,11 @@ ML4W_GTK_LISTENER="${ML4W_GTK_LISTENER:-${HOME}/.config/ml4w/listeners/gtk-theme
ML4W_LISTENERS_SCRIPT="${ML4W_LISTENERS_SCRIPT:-${HOME}/.config/ml4w/listeners.sh}"
LISTENER_BEGIN_MARK='# >>> Dreamcoder listener hook >>>'
LISTENER_END_MARK='# <<< Dreamcoder listener hook <<<'
MATUGEN_CONFIG="${MATUGEN_CONFIG:-${HOME}/.config/matugen/config.toml}"
MATUGEN_OFF_PREFIX='#dreamcoder-off# '
# Matugen templates whose output is a file Dreamcoder owns. swaync/colors.css is a symlink
# to waybar/colors.css, so its template lands on the same file.
DREAMCODER_MATUGEN_TEMPLATES="hyprland hyprland-lua waybar rofi swaync"

# Print the runner without any Dreamcoder block (marked or legacy) and without
# trailing blank lines, so re-appending always yields the same bytes.
Expand All @@ -65,14 +81,17 @@ hook_ml4w_runner() {
printf '⚠ ML4W wallpaper runner not found, skipped: %s\n' "${ML4W_WALLPAPER_SCRIPT}" >&2
return 0
fi
local current desired
# %q escapes every shell metacharacter: the path lands in generated shell source, where
# a quote, $ or backtick inside double quotes would still expand or break out.
local current desired hook_q
hook_q="$(printf '%q' "${HOOK_SCRIPT}")"
current="$(cat "${ML4W_WALLPAPER_SCRIPT}")"
desired="$(strip_runner_hook "${ML4W_WALLPAPER_SCRIPT}")

${BEGIN_MARK}
# Managed by dreamcoder-dots scripts/apply-ml4w-hooks.sh; re-run it after ML4W upgrades.
if [[ -x \"${HOOK_SCRIPT}\" ]]; then
\"${HOOK_SCRIPT}\" \"\$${ML4W_WALLPAPER_VAR}\"
if [[ -x ${hook_q} ]]; then
${hook_q} \"\$${ML4W_WALLPAPER_VAR}\"
fi
${END_MARK}"
if [[ "${current}" == "${desired}" ]]; then
Expand All @@ -92,9 +111,10 @@ ${END_MARK}"
# 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" '
DREAMCODER_DISPATCHER_Q="$(printf '%q' "${DREAMCODER_DOTS_DIR}/scripts/dreamcoder")" \
awk -v begin="${LISTENER_BEGIN_MARK}" -v end="${LISTENER_END_MARK}" '
function trim(s) { sub(/^[[:space:]]+/, "", s); sub(/[[:space:]]+$/, "", s); return s }
BEGIN { dispatcher = ENVIRON["DREAMCODER_DISPATCHER_Q"] }
trim($0) == begin { skip = 1; next }
skip && trim($0) == end { skip = 0; next }
skip { next }
Expand All @@ -108,7 +128,7 @@ render_listener_hook() {
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"
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)
Expand All @@ -120,7 +140,7 @@ render_listener_hook() {
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 </dev/null >\"$HOME/.cache/dreamcoder/ml4w-listener-sync.log\" 2>&1 || true"
print pad " DREAMCODER_THEME_MODE=\"$_dreamcoder_mode\" DREAMCODER_WRITE_REPO=0 timeout 120 " dispatcher " sync </dev/null >\"$HOME/.cache/dreamcoder/ml4w-listener-sync.log\" 2>&1 || true"
print pad "fi"
print pad end
}
Expand All @@ -130,6 +150,12 @@ render_listener_hook() {
}

hook_gtk_listener() {
# The block bounds `dreamcoder sync` with timeout(1); without it a hung sync would
# stall ML4W's listener, so leave the listener alone rather than hook it unbounded.
if ! command -v timeout >/dev/null 2>&1; then
printf '⚠ timeout(1) not found, GTK theme listener hook skipped\n' >&2
return 0
fi
if [[ ! -f "${ML4W_GTK_LISTENER}" ]]; then
printf '⚠ ML4W GTK theme listener not found, skipped: %s\n' "${ML4W_GTK_LISTENER}" >&2
return 0
Expand Down Expand Up @@ -158,6 +184,10 @@ hook_gtk_listener() {
# ~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 ! command -v timeout >/dev/null 2>&1; then
printf '⚠ timeout(1) not found, restart the GTK theme listener manually: %s --restart gtk-theme-switcher\n' "${ML4W_LISTENERS_SCRIPT}" >&2
return 0
fi
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
Expand All @@ -169,6 +199,47 @@ restart_gtk_listener() {
fi
}

# Print the Matugen config with every Dreamcoder-owned template section commented out.
# Sections that are already disabled start with '#', so re-running changes nothing.
# To undo by hand: sed -i 's/^#dreamcoder-off# //' ~/.config/matugen/config.toml
disable_owned_templates() {
awk -v owned="${DREAMCODER_MATUGEN_TEMPLATES}" -v prefix="${MATUGEN_OFF_PREFIX}" '
BEGIN { n = split(owned, names, " "); for (i = 1; i <= n; i++) want["[templates." names[i] "]"] = 1 }
/^\[/ { header = $0; sub(/[ \t]+$/, "", header); off = (header in want) }
off && $0 != "" && $0 !~ /^#/ { print prefix $0; next }
{ print }
' "$1"
}

# Matugen rewrites the colour files Dreamcoder owns on every wallpaper or mode change,
# and restoring them afterwards is a race that can be lost. Disable those templates at
# the source. ML4W upgrades restore the stock file, so re-run this script afterwards.
hook_matugen_config() {
if [[ ! -f "${MATUGEN_CONFIG}" ]]; then
printf '⚠ Matugen config not found, skipped: %s\n' "${MATUGEN_CONFIG}" >&2
return 0
fi
local current desired
current="$(cat "${MATUGEN_CONFIG}")"
desired="$(disable_owned_templates "${MATUGEN_CONFIG}")"
if [[ "${current}" == "${desired}" ]]; then
printf '✓ Matugen no longer writes Dreamcoder-owned colour files (already current)\n'
return 0
fi
# Never leave Matugen with a config it cannot parse: without a validator, leave it alone.
if ! python3 -c 'import tomllib' 2>/dev/null; then
printf '⚠ python3 with tomllib is not available to validate the Matugen config, left untouched: %s\n' "${MATUGEN_CONFIG}" >&2
return 0
fi
if ! printf '%s\n' "${desired}" | python3 -c 'import sys, tomllib; tomllib.loads(sys.stdin.read())' 2>/dev/null; then
printf '⚠ patched Matugen config is not valid TOML, left untouched: %s\n' "${MATUGEN_CONFIG}" >&2
return 0
fi
# Write through the path so a symlinked config stays a symlink.
printf '%s\n' "${desired}" >"${MATUGEN_CONFIG}"
printf '✓ Matugen templates for Dreamcoder-owned files disabled: %s\n' "${MATUGEN_CONFIG}"
}

hook_waypaper() {
if [[ ! -f "${WAYPAPER_CONFIG}" ]]; then
printf '✓ waypaper config absent (not used by ML4W 2.16), skipped\n'
Expand All @@ -178,16 +249,22 @@ hook_waypaper() {
printf '✓ waypaper post_command hook already present\n'
return 0
fi
local hook="${HOOK_SCRIPT} \"\$wallpaper\" > /dev/null 2>&1"
# `&` in a sed replacement expands to the whole match, and the hook text
# contains `2>&1`; unescaped it corrupts the line by re-inserting the match.
local hook_sed="${hook//&/\\&}"
sed -i "s|^post_command = \(.*\)|post_command = \1; ${hook_sed}|" "${WAYPAPER_CONFIG}"
# awk with ENVIRON instead of sed: the hook text carries the script path and `2>&1`, and a
# sed replacement would mangle `&`, the delimiter and backslashes.
local hook updated
hook="$(printf '%q' "${HOOK_SCRIPT}") \"\$wallpaper\" > /dev/null 2>&1"
updated="$(DREAMCODER_WAYPAPER_HOOK="${hook}" awk '
/^post_command = / { print $0 "; " ENVIRON["DREAMCODER_WAYPAPER_HOOK"]; next }
{ print }
' "${WAYPAPER_CONFIG}")"
# Write through the path so a symlinked config stays a symlink.
printf '%s\n' "${updated}" >"${WAYPAPER_CONFIG}"
printf '✓ waypaper post_command hooked: %s\n' "${WAYPAPER_CONFIG}"
}

hook_ml4w_runner
hook_gtk_listener
hook_matugen_config
hook_waypaper

"${DREAMCODER_DOTS_DIR}/scripts/theme-auto.sh"
Expand Down
89 changes: 89 additions & 0 deletions tests/fixtures/ml4w/matugen-config-2.16.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
[config]

[templates.colorsjson]
input_path = '~/.config/matugen/templates/colors.json'
output_path = '~/.config/ml4w/colors/colors.json'

[templates.kitty]
input_path = '~/.config/matugen/templates/kitty-colors.conf'
output_path = '~/.config/kitty/colors-matugen.conf'
post_hook = 'pkill -SIGUSR1 kitty'

[templates.btop]
input_path = '~/.config/matugen/templates/btop.theme'
output_path = '~/.config/btop/themes/matugen.theme'

[templates.hyprland]
input_path = '~/.config/matugen/templates/hyprland-colors.conf'
output_path = '~/.config/hypr/colors.conf'
post_hook = 'hyprctl reload'

[templates.hyprland-lua]
input_path = '~/.config/matugen/templates/hyprland-colors.lua'
output_path = '~/.config/hypr/colors.lua'
post_hook = 'hyprctl reload'

[templates.waybar]
input_path = '~/.config/matugen/templates/colors.css'
output_path = '~/.config/waybar/colors.css'

[templates.rofi]
input_path = '~/.config/matugen/templates/rofi-colors.rasi'
output_path = '~/.config/rofi/colors.rasi'

[templates.gtk3]
input_path = '~/.config/matugen/templates/gtk-colors.css'
output_path = '~/.config/gtk-3.0/colors.css'

[templates.gtk4]
input_path = '~/.config/matugen/templates/gtk-colors.css'
output_path = '~/.config/gtk-4.0/colors.css'
post_hook = '~/.config/matugen/post-hook-scripts/gtk-themes-reload.sh'

[templates.pywalfox]
input_path = '~/.config/matugen/templates/pywalfox-colors.json'
output_path = '~/.cache/wal/colors.json'

[templates.swaync]
input_path = '~/.config/matugen/templates/colors.css'
output_path = '~/.config/swaync/colors.css'

[templates.sequences]
input_path = '~/.config/matugen/templates/sequences'
output_path = '~/.cache/wal/sequences'

[templates.primary]
input_path = '~/.config/matugen/templates/primary'
output_path = '~/.config/ml4w/colors/primary'

[templates.secondary]
input_path = '~/.config/matugen/templates/secondary'
output_path = '~/.config/ml4w/colors/secondary'

[templates.on_surface]
input_path = '~/.config/matugen/templates/onsurface'
output_path = '~/.config/ml4w/colors/onsurface'

[templates.on_primary]
input_path = '~/.config/matugen/templates/onprimary'
output_path = '~/.config/ml4w/colors/onprimary'

[templates.ml4w_dotfiles_settings]
input_path = "./templates/colors.json"
output_path = "~/.local/share/ml4w-dotfiles-settings/colors/colors.json"

# Colors of the ML4W Dock (github.com/mylinuxforwork/ml4w-dock), which reads
# its own copy so it can also run without the rest of the ML4W shell. It watches the file, so no
# post_hook is needed. The path is theme.colorsFile in ~/.config/ml4w-dock/config.json.
[templates.ml4w_dock]
input_path = "./templates/colors.json"
output_path = "~/.config/ml4w-dock/colors.json"

[templates.quickshell_overview]
input_path = "./templates/quickshell-overview.qml"
output_path = "~/.local/share/quickshell-overview/common/Appearance.colors.qml"

[templates.ohmyposh]
input_path = '~/.config/matugen/templates/ohmyposh-colors.json'
output_path = '~/.config/ohmyposh/colors.json'
post_hook = "jq --slurpfile palette ~/.config/ohmyposh/colors.json '. + $palette[0]' ~/.config/ohmyposh/EDM115-newline.omp.json > /tmp/new_theme.json && mv /tmp/new_theme.json ~/.config/ohmyposh/EDM115-newline.omp.json"
Loading
Loading