Add YAML-based file installation system (issue #2137)

- Replace hardcoded Bash logic with declarative YAML configuration
- Implement user preference wizard for shell/terminal/keybindings
- Add conditional file copying based on user preferences
- Support multiple sync modes: sync, soft, hard, hard-backup, soft-backup, skip, skip-if-exists
- Implement MD5 hash comparison for idempotent backups
- Add fontconfig fontset support via II_FONTSET_NAME
- Complete coverage of all config directories and files from original script

This is an experimental feature enabled via --exp-files flag.
This commit is contained in:
Matt Van Harn
2025-10-29 15:53:01 -04:00
parent c550a792b8
commit 06775806d5
2 changed files with 509 additions and 0 deletions
+393
View File
@@ -0,0 +1,393 @@
# This script is meant to be sourced.
# It's not for directly running.
# TODO: https://github.com/end-4/dots-hyprland/issues/2137
printf "${STY_CYAN}[$0]: 3. Copying config files (experimental YAML-based)${STY_RST}\n"
# Configuration file
CONFIG_FILE="sdata/subcmd-install/3.files.yaml"
# =============================================================================
# ORIGINAL FUNCTIONS
# =============================================================================
function warning_rsync_delete(){
printf "${STY_YELLOW}"
printf "The command below uses --delete for rsync which overwrites the destination folder.\n"
printf "${STY_RST}"
}
function warning_rsync_normal(){
printf "${STY_YELLOW}"
printf "The command below uses rsync which overwrites the destination.\n"
printf "${STY_RST}"
}
function backup_clashing_targets(){
# For dirs/files under target_dir, only backup those which clashes with the ones under source_dir
# Deal with arguments
local source_dir="$1"
local target_dir="$2"
local backup_dir="$3"
# Find clash dirs/files, save as clash_list
local clash_list=()
local source_list=($(ls -A "$source_dir"))
local target_list=($(ls -A "$target_dir"))
declare -A target_map
for i in "${target_list[@]}"; do
target_map["$i"]=1
done
for i in "${source_list[@]}"; do
if [[ -n "${target_map[$i]}" ]]; then
clash_list+=("$i")
fi
done
# Construct args_includes for rsync
for i in "${clash_list[@]}"; do
current_target=$target_dir/$i
if [[ -d $current_target ]]; then
args_includes+=(--include="$current_target/")
args_includes+=(--include="$current_target/**")
else
args_includes+=(--include="$current_target")
fi
done
args_includes+=(--exclude="*")
x mkdir -p $backup_dir
x rsync -av --progress "${args_includes[@]}" "$target_dir/" "$backup_dir/"
}
function ask_backup_configs(){
printf "${STY_RED}"
printf "Would you like to backup clashing dirs/files under \"$XDG_CONFIG_HOME\" and \"$XDG_DATA_HOME\" to \"$BACKUP_DIR\"?"
read -p "[y/N] " backup_confirm
case $backup_confirm in
[yY][eE][sS]|[yY])
showfun backup_clashing_targets
v backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config"
v backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share"
;;
*) echo "Skipping backup..." ;;
esac
printf "${STY_RST}"
}
# =============================================================================
# CONFIGURATION FUNCTIONS
# =============================================================================
# User preference wizard
wizard_update_preferences() {
echo -e "${STY_CYAN}=== Dotfiles Customization ===${STY_RESET}"
# Get current preferences
current_shell=$(yq '.user_preferences.shell // "fish"' "$CONFIG_FILE")
current_terminal=$(yq '.user_preferences.terminal // "kitty"' "$CONFIG_FILE")
current_keybindings=$(yq '.user_preferences.keybindings // "default"' "$CONFIG_FILE")
echo "Current preferences:"
echo " Shell: $current_shell"
echo " Terminal: $current_terminal"
echo " Keybindings: $current_keybindings"
echo
# Shell selection
echo "Which shell do you prefer?"
echo "1) fish (default)"
echo "2) zsh"
read -p "Enter choice [1-2]: " shell_choice
case "$shell_choice" in
1|"") shell="fish" ;;
2) shell="zsh" ;;
*) echo "Invalid choice, using fish"; shell="fish" ;;
esac
# Terminal selection
echo
echo "Which terminal do you prefer?"
echo "1) kitty (default)"
echo "2) foot"
read -p "Enter choice [1-2]: " terminal_choice
case "$terminal_choice" in
1|"") terminal="kitty" ;;
2) terminal="foot" ;;
*) echo "Invalid choice, using kitty"; terminal="kitty" ;;
esac
# Keybindings selection
echo
echo "Which keybinding style do you prefer?"
echo "1) default (arrow keys)"
echo "2) vim (H/J/K/L)"
read -p "Enter choice [1-2]: " keybind_choice
case "$keybind_choice" in
1|"") keybindings="default" ;;
2) keybindings="vim" ;;
*) echo "Invalid choice, using default"; keybindings="default" ;;
esac
# Update YAML in-place
yq -i ".user_preferences.shell = \"$shell\"" "$CONFIG_FILE"
yq -i ".user_preferences.terminal = \"$terminal\"" "$CONFIG_FILE"
yq -i ".user_preferences.keybindings = \"$keybindings\"" "$CONFIG_FILE"
echo
echo "Preferences updated!"
}
# Get user preference
get_pref() {
yq -r ".user_preferences.$1" "$CONFIG_FILE"
}
# Check if pattern should be processed based on user preferences
should_process_pattern() {
local pattern="$1"
local condition=$(echo "$pattern" | yq '.condition // "true"')
# If no condition or condition is "true", always process
if [[ "$condition" == "true" ]]; then
return 0
fi
# Extract the preference type and value from condition
local type=$(echo "$condition" | yq '.type')
local value=$(echo "$condition" | yq '.value')
[[ "$(get_pref "$type")" == "$value" ]]
}
# Compare hashes of files/directories, return true if they are the same, false otherwise
files_are_same() {
local path1="$1"
local path2="$2"
# Check if paths exist
if [[ ! -e "$path1" || ! -e "$path2" ]]; then
return 1
fi
# For directories, use find + md5sum to compare recursively
# For files, use md5sum directly
if [[ -d "$path1" && -d "$path2" ]]; then
# Compare directory contents using find and md5sum
local hash1=$(find "$path1" -type f -exec md5sum {} \; | sort -k 2 | md5sum | awk '{print $1}')
local hash2=$(find "$path2" -type f -exec md5sum {} \; | sort -k 2 | md5sum | awk '{print $1}')
[[ "$hash1" == "$hash2" ]]
elif [[ -f "$path1" && -f "$path2" ]]; then
# Compare file hashes
local hash1=$(md5sum "$path1" | awk '{print $1}')
local hash2=$(md5sum "$path2" | awk '{print $1}')
[[ "$hash1" == "$hash2" ]]
else
# One is a file, one is a directory - different types
return 1
fi
}
# Find next backup number
get_next_backup_number() {
local base_path="$1"
local counter=1
while [[ -e "${base_path}.old.${counter}" ]]; do
((counter++))
done
echo $counter
}
# =============================================================================
# MAIN EXECUTION
# =============================================================================
# Ensure directories exist
v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME
# Handle backup
case $ask in
false) sleep 0 ;;
*) ask_backup_configs ;;
esac
# Run user preference wizard
case $ask in
false) sleep 0 ;;
*) wizard_update_preferences ;;
esac
# Read patterns from YAML file
readarray patterns < <(yq -o=j -I=0 '.patterns[]' "$CONFIG_FILE")
# Process each pattern
for pattern in "${patterns[@]}"; do
from=$(echo "$pattern" | yq '.from' - | envsubst)
to=$(echo "$pattern" | yq '.to' - | envsubst)
mode=$(echo "$pattern" | yq '.mode' - | envsubst)
condition=$(echo "$pattern" | yq '.condition // "true"')
# Handle fontconfig fontset override
# If II_FONTSET_NAME is set and this is the fontconfig pattern, use the fontset instead
if [[ "$from" == "dots/.config/fontconfig" ]] && [[ -n "${II_FONTSET_NAME:-}" ]]; then
from="dots-extra/fontsets/${II_FONTSET_NAME}"
echo "Using fontset \"${II_FONTSET_NAME}\" for fontconfig"
fi
# Check if pattern should be processed
if ! should_process_pattern "$pattern"; then
# Format condition message nicely
if [[ "$condition" != "true" ]]; then
cond_type=$(echo "$condition" | yq -r '.type // ""')
cond_value=$(echo "$condition" | yq -r '.value // ""')
if [[ -n "$cond_type" && -n "$cond_value" ]]; then
echo "Skipping $from -> $to (condition not met: $cond_type == '$cond_value')"
else
echo "Skipping $from -> $to (condition not met)"
fi
else
echo "Skipping $from -> $to (condition not met)"
fi
continue
fi
echo "Processing: $from -> $to (mode: $mode)"
# Build exclude arguments for rsync
excludes=()
if echo "$pattern" | yq -e '.excludes' >/dev/null 2>&1; then
while IFS= read -r exclude; do
excludes+=(--exclude "$exclude")
done < <(echo "$pattern" | yq -r '.excludes[]')
fi
# Check if source exists
if [[ ! -e "$from" ]]; then
echo "Warning: Source does not exist: $from (skipping)"
continue
fi
# Ensure destination directory exists for files
if [[ -f "$from" ]]; then
v mkdir -p "$(dirname "$to")"
fi
# Execute based on mode
case $mode in
"sync")
if [[ -d "$from" ]]; then
warning_rsync_delete
v rsync -av --delete "${excludes[@]}" "$from/" "$to/"
else
warning_rsync_normal
# For files, don't use trailing slash and don't use --delete
v rsync -av "${excludes[@]}" "$from" "$to"
fi
;;
"soft")
warning_rsync_normal
if [[ -d "$from" ]]; then
v rsync -av "${excludes[@]}" "$from/" "$to/"
else
# For files, don't use trailing slash
v rsync -av "${excludes[@]}" "$from" "$to"
fi
;;
"hard")
v cp -r "$from" "$to"
;;
"hard-backup")
if [[ -e "$to" ]]; then
if files_are_same "$from" "$to"; then
echo "Files are identical, skipping backup"
else
backup_number=$(get_next_backup_number "$to")
v mv "$to" "$to.old.$backup_number"
v cp -r "$from" "$to"
fi
else
v cp -r "$from" "$to"
fi
;;
"soft-backup")
if [[ -e "$to" ]]; then
if files_are_same "$from" "$to"; then
echo "Files are identical, skipping backup"
else
v cp -r "$from" "$to.new"
fi
else
v cp -r "$from" "$to"
fi
;;
"skip")
echo "Skipping $from"
;;
"skip-if-exists")
if [[ -e "$to" ]]; then
echo "Skipping $from (destination exists)"
else
v cp -r "$from" "$to"
fi
;;
*)
echo "Unknown mode: $mode"
;;
esac
done
# Prevent hyprland from not fully loaded
sleep 1
try hyprctl reload
# Rest of original script logic...
# (Keep the existing warning messages and file checks)
warn_files=()
warn_files_tests=()
warn_files_tests+=(/usr/local/lib/{GUtils-1.0.typelib,Gvc-1.0.typelib,libgutils.so,libgvc.so})
warn_files_tests+=(/usr/local/share/fonts/TTF/Rubik{,-Italic}'[wght]'.ttf)
warn_files_tests+=(/usr/local/share/licenses/ttf-rubik)
warn_files_tests+=(/usr/local/share/fonts/TTF/Gabarito-{Black,Bold,ExtraBold,Medium,Regular,SemiBold}.ttf)
warn_files_tests+=(/usr/local/share/licenses/ttf-gabarito)
warn_files_tests+=(/usr/local/share/icons/OneUI{,-dark,-light})
warn_files_tests+=(/usr/local/share/icons/Bibata-Modern-Classic)
warn_files_tests+=(/usr/local/bin/{LaTeX,res})
for i in ${warn_files_tests[@]}; do
echo $i
test -f $i && warn_files+=($i)
test -d $i && warn_files+=($i)
done
#####################################################################################
# TODO: output the logs below to a temp file and cat that file, also show the path of the file so users will be able to read it again.
printf "\n"
printf "\n"
printf "\n"
printf "${STY_CYAN}[$0]: Finished${STY_RESET}\n"
printf "\n"
printf "${STY_CYAN}When starting Hyprland from your display manager (login screen) ${STY_RED} DO NOT SELECT UWSM ${STY_RESET}\n"
printf "\n"
printf "${STY_CYAN}If you are already running Hyprland,${STY_RESET}\n"
printf "${STY_CYAN}Press ${STY_BG_CYAN} Ctrl+Super+T ${STY_BG_CYAN} to select a wallpaper${STY_RESET}\n"
printf "${STY_CYAN}Press ${STY_BG_CYAN} Super+/ ${STY_CYAN} for a list of keybinds${STY_RESET}\n"
printf "\n"
printf "${STY_CYAN}For suggestions/hints after installation:${STY_RESET}\n"
printf "${STY_CYAN}${STY_UNDERLINE} https://ii.clsty.link/en/ii-qs/01setup/#post-installation ${STY_RESET}\n"
printf "\n"
if [[ -z "${ILLOGICAL_IMPULSE_VIRTUAL_ENV}" ]]; then
printf "\n${STY_RED}[$0]: \!! Important \!! : Please ensure environment variable ${STY_RESET} \$ILLOGICAL_IMPULSE_VIRTUAL_ENV ${STY_RED} is set to proper value (by default \"~/.local/state/quickshell/.venv\"), or Quickshell config will not work. We have already provided this configuration in ~/.config/hypr/hyprland/env.conf, but you need to ensure it is included in hyprland.conf, and also a restart is needed for applying it.${STY_RESET}\n"
fi
if [[ ! -z "${warn_files[@]}" ]]; then
printf "\n${STY_RED}[$0]: \!! Important \!! : Please delete ${STY_RESET} ${warn_files[*]} ${STY_RED} manually as soon as possible, since we\'re now using AUR package or local PKGBUILD to install them for Arch(based) Linux distros, and they'll take precedence over our installation, or at least take up more space.${STY_RESET}\n"
fi
+116
View File
@@ -0,0 +1,116 @@
version: "1.0"
user_preferences:
shell: "fish" # fish | zsh
terminal: "foot" # kitty | foot
keybindings: "default" # default | vim
patterns:
# Always install these files
- from: "dots/.config/quickshell"
to: "$XDG_CONFIG_HOME/quickshell"
mode: "sync"
# Conditionally install these files
- from: "dots/.config/fish"
to: "$XDG_CONFIG_HOME/fish"
mode: "sync"
condition:
type: "shell"
value: "fish"
- from: "dots/.config/zshrc.d"
to: "$XDG_CONFIG_HOME/zshrc.d"
mode: "sync"
condition:
type: "shell"
value: "zsh"
- from: "dots/.config/foot"
to: "$XDG_CONFIG_HOME/foot"
mode: "sync"
condition:
type: "terminal"
value: "foot"
- from: "dots/.config/kitty"
to: "$XDG_CONFIG_HOME/kitty"
mode: "sync"
condition:
type: "terminal"
value: "kitty"
# Hyprland
- from: "dots/.config/hypr"
to: "$XDG_CONFIG_HOME/hypr"
mode: "sync"
excludes: ["custom", "hyprlock.conf", "hypridle.conf", "hyprland.conf"]
# Hyprland special files
- from: "dots/.config/hypr/hyprland.conf"
to: "$XDG_CONFIG_HOME/hypr/hyprland.conf"
mode: "hard-backup"
- from: "dots/.config/hypr/hypridle.conf"
to: "$XDG_CONFIG_HOME/hypr/hypridle.conf"
mode: "soft-backup"
- from: "dots/.config/hypr/hyprlock.conf"
to: "$XDG_CONFIG_HOME/hypr/hyprlock.conf"
mode: "soft-backup"
- from: "dots/.config/hypr/custom"
to: "$XDG_CONFIG_HOME/hypr/custom"
mode: "skip-if-exists"
- from: "dots/.local/share/icons"
to: "$XDG_DATA_HOME/icons"
mode: "soft"
- from: "dots/.local/share/konsole"
to: "$XDG_DATA_HOME/konsole"
mode: "soft"
# Fontconfig (default - fontsets handled separately if II_FONTSET_NAME is set)
- from: "dots/.config/fontconfig"
to: "$XDG_CONFIG_HOME/fontconfig"
mode: "sync"
# MISC config directories (other .config directories)
- from: "dots/.config/fuzzel"
to: "$XDG_CONFIG_HOME/fuzzel"
mode: "sync"
- from: "dots/.config/kde-material-you-colors"
to: "$XDG_CONFIG_HOME/kde-material-you-colors"
mode: "sync"
- from: "dots/.config/Kvantum"
to: "$XDG_CONFIG_HOME/Kvantum"
mode: "sync"
- from: "dots/.config/matugen"
to: "$XDG_CONFIG_HOME/matugen"
mode: "sync"
- from: "dots/.config/mpv"
to: "$XDG_CONFIG_HOME/mpv"
mode: "sync"
- from: "dots/.config/qt5ct"
to: "$XDG_CONFIG_HOME/qt5ct"
mode: "sync"
- from: "dots/.config/qt6ct"
to: "$XDG_CONFIG_HOME/qt6ct"
mode: "sync"
- from: "dots/.config/wlogout"
to: "$XDG_CONFIG_HOME/wlogout"
mode: "sync"
- from: "dots/.config/xdg-desktop-portal"
to: "$XDG_CONFIG_HOME/xdg-desktop-portal"
mode: "sync"
# MISC config files (individual files in .config)
- from: "dots/.config/chrome-flags.conf"
to: "$XDG_CONFIG_HOME/chrome-flags.conf"
mode: "soft"
- from: "dots/.config/code-flags.conf"
to: "$XDG_CONFIG_HOME/code-flags.conf"
mode: "soft"
- from: "dots/.config/darklyrc"
to: "$XDG_CONFIG_HOME/darklyrc"
mode: "soft"
- from: "dots/.config/dolphinrc"
to: "$XDG_CONFIG_HOME/dolphinrc"
mode: "soft"
- from: "dots/.config/kdeglobals"
to: "$XDG_CONFIG_HOME/kdeglobals"
mode: "soft"
- from: "dots/.config/konsolerc"
to: "$XDG_CONFIG_HOME/konsolerc"
mode: "soft"
- from: "dots/.config/starship.toml"
to: "$XDG_CONFIG_HOME/starship.toml"
mode: "soft"
- from: "dots/.config/thorium-flags.conf"
to: "$XDG_CONFIG_HOME/thorium-flags.conf"
mode: "soft"