Dotfiles/desktopenvs/hyprlua/config-updater/update-configs.sh

166 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# update-configs — sync dotfiles configs into ~/.config
#
# Config: ~/.config/config-updater/updater.conf
# Manifest: ~/.config/config-updater/manifest
#
# Syntax in updater.conf:
# config <name> [except <sub1> <sub2> ...]
# copy SOURCE_BASE/<name> to ~/.config/<name>, optionally skipping subdirs
# flat <name>
# copy contents of SOURCE_BASE/<name> directly into ~/.config/
# ignore <name>
# present in source but intentionally not managed here
set -euo pipefail
CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/config-updater"
CONF_FILE="${CONF_DIR}/updater.conf"
MANIFEST="${CONF_DIR}/manifest"
TARGET="${XDG_CONFIG_HOME:-$HOME/.config}"
RED='\033[0;31m'; YEL='\033[1;33m'; GRN='\033[0;32m'; DIM='\033[2m'; RST='\033[0m'
err() { printf "${RED}✖ %s${RST}\n" "$*" >&2; }
warn() { printf "${YEL}⚠ %s${RST}\n" "$*"; }
ok() { printf "${GRN}✔ %s${RST}\n" "$*"; }
note() { printf "${DIM} %s${RST}\n" "$*"; }
die() { err "$*"; exit 1; }
[[ -f "$CONF_FILE" ]] || die "Config not found: $CONF_FILE"
# ── parse updater.conf ────────────────────────────────────────────────────────
SOURCE_BASE=""
declare -A ENTRY_TYPE # name → config | flat | ignore
declare -A ENTRY_EXCL # name → space-separated list of excluded subdirs
while IFS= read -r line; do
line="${line%%#*}"
line="${line#"${line%%[! ]*}"}"
line="${line%"${line##*[! ]}"}"
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^SOURCE_BASE[[:space:]]*=[[:space:]]*(.+)$ ]]; then
src="${BASH_REMATCH[1]%"${BASH_REMATCH[1]##*[! ]}"}"
SOURCE_BASE="${src/#\~/$HOME}"
continue
fi
read -r type name rest <<< "$line"
[[ -z "${name:-}" ]] && continue
case "$type" in
config|flat|ignore)
ENTRY_TYPE["$name"]="$type"
# Parse optional "except sub1 sub2 ..."
if [[ "${rest:-}" =~ ^except[[:space:]]+(.+)$ ]]; then
ENTRY_EXCL["$name"]="${BASH_REMATCH[1]}"
else
ENTRY_EXCL["$name"]=""
fi
;;
*) warn "Unknown type '$type' for '$name' — skipping" ;;
esac
done < "$CONF_FILE"
[[ -n "$SOURCE_BASE" ]] || die "SOURCE_BASE not defined in $CONF_FILE"
[[ -d "$SOURCE_BASE" ]] || die "SOURCE_BASE not found: $SOURCE_BASE"
# ── load previous manifest ────────────────────────────────────────────────────
declare -A PREV_MANIFEST
if [[ -f "$MANIFEST" ]]; then
while IFS=: read -r mtype mname; do
[[ -n "${mtype:-}" && -n "${mname:-}" ]] && PREV_MANIFEST["$mname"]="$mtype"
done < "$MANIFEST"
fi
warned=0
# ── warn: manifest entries no longer in updater.conf ─────────────────────────
for mname in "${!PREV_MANIFEST[@]}"; do
[[ -n "${ENTRY_TYPE[$mname]:-}" ]] && continue
warn "'$mname' was previously managed but is no longer in updater.conf"
note "Remove ~/.config/$mname manually if it is no longer needed"
warned=1
done
# ── warn: source items not covered by updater.conf ───────────────────────────
while IFS= read -r -d '' item; do
name="$(basename "$item")"
[[ "$name" == .* ]] && continue
[[ -n "${ENTRY_TYPE[$name]:-}" ]] && continue
warn "Untracked source item: '$name' — add to updater.conf (config / flat / ignore)"
warned=1
done < <(find "$SOURCE_BASE" -maxdepth 1 -mindepth 1 -print0 | sort -z)
(( warned )) && printf '\n'
# ── apply ─────────────────────────────────────────────────────────────────────
errors=0
new_manifest=()
for name in "${!ENTRY_TYPE[@]}"; do
type="${ENTRY_TYPE[$name]}"
[[ "$type" == ignore ]] && continue
src="${SOURCE_BASE}/${name}"
if [[ ! -e "$src" ]]; then
err "Source missing: $src"
(( errors++ )) || true
continue
fi
excl="${ENTRY_EXCL[$name]:-}"
case "$type" in
config)
if [[ -z "$excl" ]]; then
rm -rf "${TARGET:?}/${name}"
cp -r "$src" "$TARGET/$name"
ok "config $name"
else
# Copy all top-level items of $src except excluded subdirs
mkdir -p "${TARGET}/${name}"
item_errors=0
while IFS= read -r -d '' item; do
item_name="$(basename "$item")"
skip_item=false
for e in $excl; do
[[ "$item_name" == "$e" ]] && skip_item=true && break
done
[[ "$skip_item" == true ]] && continue
rm -rf "${TARGET:?}/${name}/${item_name}"
cp -r "$item" "${TARGET}/${name}/" || (( item_errors++ )) || true
done < <(find "$src" -maxdepth 1 -mindepth 1 -print0 | sort -z)
if (( item_errors > 0 )); then
err "config $name (${item_errors} error(s))"
(( errors++ )) || true
else
ok "config $name (excl: $excl)"
fi
fi
;;
flat)
[[ -d "$src" ]] || {
err "flat entry '$name' must be a directory: $src"
(( errors++ )) || true
continue
}
cp -r "${src}/." "$TARGET/"
ok "flat $name → (contents into ~/.config/)"
;;
esac
new_manifest+=("${type}:${name}")
done
# ── write manifest ────────────────────────────────────────────────────────────
printf '%s\n' "${new_manifest[@]}" | sort > "$MANIFEST"
printf '\n'
if (( errors > 0 )); then
err "$errors error(s) — manifest may be incomplete"
exit 1
else
ok "Done — manifest updated at $MANIFEST"
fi