diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/fix-systemsettings-lua.sh | 229 | ||||
| -rw-r--r-- | scripts/lib_apps.sh | 66 | ||||
| -rw-r--r-- | scripts/lib_copy.sh | 116 | ||||
| -rwxr-xr-x | scripts/migrate-hypr-to-lua.sh | 2212 |
4 files changed, 2595 insertions, 28 deletions
diff --git a/scripts/fix-systemsettings-lua.sh b/scripts/fix-systemsettings-lua.sh new file mode 100755 index 00000000..e1b550fc --- /dev/null +++ b/scripts/fix-systemsettings-lua.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# ================================================== +# KoolDots (2026) +# Project URL: https://github.com/LinuxBeginnings +# License: GNU GPLv3 +# SPDX-License-Identifier: GPL-3.0-or-later +# ================================================== +# Regenerate ~/.config/hypr/configs/system_settings.lua from SystemSettings.conf. + +set -euo pipefail + +CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" +HYPR_DIR="${CONFIG_HOME}/hypr" +CONFIGS_DIR="${HYPR_DIR}/configs" +LEGACY_DIR="${CONFIGS_DIR}/LegacyConfigs" +SYSTEM_SETTINGS_CONF="${CONFIGS_DIR}/SystemSettings.conf" +SYSTEM_SETTINGS_LUA="${CONFIGS_DIR}/system_settings.lua" + +python3 - <<'PY' +from pathlib import Path +import os +import re + +HEADER = """-- ================================================== +-- KoolDots (2026) +-- Project URL: https://github.com/LinuxBeginnings +-- License: GNU GPLv3 +-- SPDX-License-Identifier: GPL-3.0-or-later +-- ================================================== +""" + +config_home = Path(os.getenv("XDG_CONFIG_HOME") or (Path.home() / ".config")) +hypr_dir = config_home / "hypr" +configs_dir = hypr_dir / "configs" +legacy_dir = configs_dir / "LegacyConfigs" +system_settings_path = configs_dir / "SystemSettings.conf" +output_path = configs_dir / "system_settings.lua" + +def strip_comment(line: str) -> str: + return line.split("#", 1)[0].strip() + +def lua_string(value: str) -> str: + return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' + +def scalar(value: str, *, bool_words: bool = True) -> str: + value = value.strip() + lower = value.lower() + if bool_words and lower in {"on", "true", "yes"}: + return "true" + if bool_words and lower in {"off", "false", "no"}: + return "false" + if re.fullmatch(r"[-+]?\d+(\.\d+)?", value): + return value + return lua_string(value) + +def render_table(value, indent=4): + lines = [] + for key, child in value.items(): + if isinstance(child, dict): + lines.append(" " * indent + f"{key} = {{") + lines.extend(render_table(child, indent + 2)) + lines.append(" " * indent + "},") + else: + lines.append(" " * indent + f"{key} = {scalar(str(child))},") + return lines + +def latest_legacy_file(path: Path): + if not legacy_dir.is_dir(): + return None + candidates = [] + for snapshot in sorted(legacy_dir.iterdir()): + if not snapshot.is_dir(): + continue + candidate = snapshot / path.name + if candidate.is_file(): + candidates.append(candidate) + return candidates[-1] if candidates else None + +def parse_hyprlang_sections(path: Path): + sections = {} + if path is None or not path.exists(): + return sections + stack = [sections] + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + if line.startswith("source"): + continue + if line.endswith("{"): + name = line[:-1].strip().replace("-", "_") + if not name or name.startswith("$"): + continue + current = stack[-1] + target = current.setdefault(name, {}) + stack.append(target) + continue + if line == "}": + if len(stack) > 1: + stack.pop() + continue + if "=" in line: + key, value = [part.strip() for part in line.split("=", 1)] + if not key or key.startswith("$"): + continue + key = key.replace("-", "_") + container = stack[-1] + parts = [part.replace("-", "_") for part in key.split(".") if part] + for part in parts[:-1]: + container = container.setdefault(part, {}) + container[parts[-1]] = value + return sections + +def parse_scripts_dir(path: Path): + if path is None or not path.exists(): + return None + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^\$scriptsDir\s*=\s*(.+)$", line) + if match: + value = match.group(1).strip().strip('"').strip("'") + return value + return None + +def parse_gestures(path: Path): + simple = [] + complex_entries = [] + if path is None or not path.exists(): + return simple, complex_entries + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^gesture\s*=\s*(.+)$", line) + if not match: + continue + payload = match.group(1).strip() + parts = [part.strip() for part in payload.split(",")] + if len(parts) >= 3 and parts[2] == "workspace": + try: + fingers = int(parts[0]) + except ValueError: + complex_entries.append(payload) + continue + simple.append({"fingers": fingers, "direction": parts[1], "action": "workspace"}) + else: + complex_entries.append(payload) + return simple, complex_entries + +source_path = system_settings_path if system_settings_path.exists() else latest_legacy_file(system_settings_path) + +lines = [ + "-- System Settings (auto-generated).", + "-- This file keeps migrated settings split from user overrides.", + "-- Add only Lua entries here.", + "-- Example:", + "-- hl.config({ general = { gaps_in = 4, gaps_out = 8 } })", + "", +] + +if source_path is None: + lines.append("-- No active entries were found in SystemSettings.conf.") +else: + scripts_dir = parse_scripts_dir(source_path) + if scripts_dir: + lines.append(f"local scriptsDir = {lua_string(scripts_dir)}") + lines.append("") + + sections = parse_hyprlang_sections(source_path) + gestures_section = sections.pop("gestures", None) + if gestures_section and "gesture" in gestures_section: + gestures_section.pop("gesture", None) + + ordered_sections = [ + "dwindle", + "master", + "scrolling", + "general", + "input", + "gestures", + "misc", + "binds", + "xwayland", + "render", + "cursor", + ] + for section in ordered_sections: + if section == "gestures": + data = gestures_section + elif section == "misc": + data = sections.get(section) or {} + if "force_default_wallpaper" not in data: + data["force_default_wallpaper"] = "false" + else: + data = sections.get(section) + if not data: + continue + lines.append("hl.config({") + lines.append(f" {section} = {{") + lines.extend(render_table(data, indent=4)) + lines.append(" },") + lines.append("})") + lines.append("") + + simple_gestures, complex_gestures = parse_gestures(source_path) + for spec in simple_gestures: + lines.extend([ + "hl.gesture({", + f" fingers = {spec['fingers']},", + f" direction = {lua_string(spec['direction'])},", + f" action = {lua_string(spec['action'])},", + "})", + "", + ]) + + if complex_gestures: + lines.append("-- Complex dispatcher gestures from SystemSettings.conf are pending explicit Lua API parity:") + for entry in complex_gestures: + lines.append(f"-- gesture = {entry}") + lines.append("") + +content = "\n".join(lines).rstrip() + "\n" +if not content.startswith(HEADER): + content = HEADER + "\n" + content.lstrip("\n") +output_path.write_text(content, encoding="utf-8") +print(f"[OK] Wrote {output_path}") +PY diff --git a/scripts/lib_apps.sh b/scripts/lib_apps.sh index 9472831d..579b5fd2 100644 --- a/scripts/lib_apps.sh +++ b/scripts/lib_apps.sh @@ -109,10 +109,43 @@ install_terminal_configs() { choose_default_editor() { local log="$1" local base="${DOTFILES_DIR:-.}" + local defaults_file="$base/config/hypr/UserConfigs/01-UserDefaults.conf" local editor_set=0 + set_env_default() { + local var_name="$1" + local value="$2" + local note="$3" + local tmp_file + tmp_file=$(mktemp) + + awk -v var_name="$var_name" -v value="$value" -v note="$note" ' + BEGIN { updated = 0 } + { + if ($0 ~ "^[[:space:]#]*env[[:space:]]*=[[:space:]]*" var_name ",") { + if (value != "") { + print "env = " var_name "," value note + } else { + print "#env = " var_name "," note + } + updated = 1 + } else { + print $0 + } + } + END { + if (!updated) { + if (value != "") { + print "env = " var_name "," value note + } else { + print "#env = " var_name "," note + } + } + } + ' "$defaults_file" > "$tmp_file" && mv "$tmp_file" "$defaults_file" + } update_editor() { local editor=$1 - sed -i "s/#env = EDITOR,.*/env = EDITOR,$editor #default editor/" "$base/config/hypr/UserConfigs/01-UserDefaults.conf" + set_env_default "EDITOR" "$editor" " #default editor" echo "${OK:-[OK]} Default editor set to ${MAGENTA:-}$editor${RESET:-}." 2>&1 | tee -a "$log" } if command -v nvim &>/dev/null; then @@ -134,6 +167,37 @@ choose_default_editor() { fi fi fi + + local visual_choice="" + local visual_value="" + local prompt_visual_input=0 + if [[ -n "${VISUAL:-}" ]]; then + printf "${INFO:-[INFO]} VISUAL is currently set to ${MAGENTA:-}%s${RESET:-}\\n" "${VISUAL}" + if read -r -p "${CAT:-[ACTION]} Use this as Quick Settings VISUAL editor? (Y/n): " visual_choice </dev/tty; then + if [[ "$visual_choice" != [Nn]* ]]; then + visual_value="${VISUAL}" + else + prompt_visual_input=1 + fi + fi + else + prompt_visual_input=1 + fi + + if [[ "$prompt_visual_input" -eq 1 ]]; then + printf "${INFO:-[INFO]} Optional GUI editor for Quick Settings (examples: neovide, geany, code, codium).\\n" + if read -r -p "${CAT:-[ACTION]} Enter VISUAL editor command or leave empty to skip: " visual_choice </dev/tty; then + visual_value="$visual_choice" + fi + fi + + if [[ -n "$visual_value" ]]; then + set_env_default "VISUAL" "$visual_value" " #default visual editor for quick settings (optional)" + echo "${OK:-[OK]} VISUAL editor set to ${MAGENTA:-}$visual_value${RESET:-}." 2>&1 | tee -a "$log" + else + set_env_default "VISUAL" "" " #default visual editor for quick settings (optional)" + echo "${NOTE:-[NOTE]} VISUAL editor not set (optional)." 2>&1 | tee -a "$log" + fi } # Install waybar-weather on non-NixOS: prefer Arch AUR, otherwise copy prebuilt asset to /usr/bin diff --git a/scripts/lib_copy.sh b/scripts/lib_copy.sh index 2eb02671..9a1c0444 100644 --- a/scripts/lib_copy.sh +++ b/scripts/lib_copy.sh @@ -119,7 +119,7 @@ copy_waybar() { copy_phase2() { local log="$1" local base="${DOTFILES_DIR:-.}" - local DIR="btop cava hypr Kvantum qt5ct qt6ct swappy wallust wlogout" + local DIR="btop cava hypr Kvantum qt5ct qt6ct swappy wallust wlogout yazi" for DIR_NAME in $DIR; do local DIRPATH="$HOME/.config/$DIR_NAME" if [ -d "$DIRPATH" ]; then @@ -143,43 +143,83 @@ restore_hypr_assets() { local express_mode="$2" local HYPR_DIR="$HOME/.config/hypr" + local CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" local BACKUP_DIR BACKUP_DIR=$(get_backup_dirname) local BACKUP_HYPR_PATH="$HYPR_DIR-backup-$BACKUP_DIR" if [ -d "$BACKUP_HYPR_PATH" ]; then - if [ "$express_mode" -eq 1 ]; then - echo "${NOTE:-[NOTE]} Express mode: skipping automatic restoration of animations and monitor profiles." 2>&1 | tee -a "$log" - return + local backup_mode="conf" + if [ -f "$BACKUP_HYPR_PATH/hyprland.lua" ] || [ -f "$CONFIG_HOME/hyprland.lua" ]; then + backup_mode="lua" fi - echo -e "\n${NOTE:-[NOTE]} Restoring ${SKY_BLUE:-}Animations & Monitor Profiles${RESET:-} into ${YELLOW:-}$HYPR_DIR${RESET:-}..." + # Preserve active Lua entrypoint automatically to avoid dropping users + # back to hyprland.conf after an upgrade. + if [ -f "$BACKUP_HYPR_PATH/hyprland.lua" ]; then + cp -f "$BACKUP_HYPR_PATH/hyprland.lua" "$HYPR_DIR/hyprland.lua" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - Restored file: ${MAGENTA:-}hyprland.lua${RESET:-}" 2>&1 | tee -a "$log" + fi - # Fresh installs should apply repo defaults; do not restore a previous wallpaper. - # RUN_MODE is set by copy.sh (install|upgrade|express) and is visible here. - local DIR_B=("Monitor_Profiles" "animations") - if [ "${RUN_MODE:-}" != "install" ]; then - DIR_B+=("wallpaper_effects") + if [ "$express_mode" -eq 1 ]; then + echo "${NOTE:-[NOTE]} Express mode: skipping automatic restoration of animations and monitor profile directories." 2>&1 | tee -a "$log" else - echo "${NOTE:-[NOTE]} Fresh install: skipping restore of wallpaper_effects so default wallpaper applies." 2>&1 | tee -a "$log" + echo -e "\n${NOTE:-[NOTE]} Restoring ${SKY_BLUE:-}Animations & Monitor Profiles${RESET:-} into ${YELLOW:-}$HYPR_DIR${RESET:-}..." + + # Fresh installs should apply repo defaults; do not restore a previous wallpaper. + # RUN_MODE is set by copy.sh (install|upgrade|express) and is visible here. + local DIR_B=("Monitor_Profiles" "animations") + if [ "${RUN_MODE:-}" != "install" ]; then + DIR_B+=("wallpaper_effects") + else + echo "${NOTE:-[NOTE]} Fresh install: skipping restore of wallpaper_effects so default wallpaper applies." 2>&1 | tee -a "$log" + fi + + for DIR_RESTORE in "${DIR_B[@]}"; do + local BACKUP_SUBDIR="$BACKUP_HYPR_PATH/$DIR_RESTORE" + if [ -d "$BACKUP_SUBDIR" ]; then + cp -r "$BACKUP_SUBDIR" "$HYPR_DIR/" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - Restored directory: ${MAGENTA:-}$DIR_RESTORE${RESET:-}" 2>&1 | tee -a "$log" + fi + done fi - for DIR_RESTORE in "${DIR_B[@]}"; do - local BACKUP_SUBDIR="$BACKUP_HYPR_PATH/$DIR_RESTORE" - if [ -d "$BACKUP_SUBDIR" ]; then - cp -r "$BACKUP_SUBDIR" "$HYPR_DIR/" 2>&1 | tee -a "$log" - echo "${OK:-[OK]} - Restored directory: ${MAGENTA:-}$DIR_RESTORE${RESET:-}" 2>&1 | tee -a "$log" + # Keep monitor/workspace state across upgrades, including express mode. + if [ "$backup_mode" = "lua" ]; then + local LUA_USER_DIR="$HYPR_DIR/UserConfigs" + mkdir -p "$LUA_USER_DIR" + + local BACKUP_LUA_MONITORS="" + local BACKUP_LUA_WORKSPACES="" + if [ -f "$BACKUP_HYPR_PATH/UserConfigs/monitors.lua" ]; then + BACKUP_LUA_MONITORS="$BACKUP_HYPR_PATH/UserConfigs/monitors.lua" + elif [ -f "$BACKUP_HYPR_PATH/lua/monitors.lua" ]; then + BACKUP_LUA_MONITORS="$BACKUP_HYPR_PATH/lua/monitors.lua" + fi + if [ -f "$BACKUP_HYPR_PATH/UserConfigs/workspaces.lua" ]; then + BACKUP_LUA_WORKSPACES="$BACKUP_HYPR_PATH/UserConfigs/workspaces.lua" + elif [ -f "$BACKUP_HYPR_PATH/lua/workspaces.lua" ]; then + BACKUP_LUA_WORKSPACES="$BACKUP_HYPR_PATH/lua/workspaces.lua" fi - done - local FILE_B=("monitors.conf" "workspaces.conf") - for FILE_RESTORE in "${FILE_B[@]}"; do - local BACKUP_FILE="$BACKUP_HYPR_PATH/$FILE_RESTORE" - if [ -f "$BACKUP_FILE" ]; then - cp "$BACKUP_FILE" "$HYPR_DIR/$FILE_RESTORE" 2>&1 | tee -a "$log" - echo "${OK:-[OK]} - Restored file: ${MAGENTA:-}$FILE_RESTORE${RESET:-}" 2>&1 | tee -a "$log" + if [ -n "$BACKUP_LUA_MONITORS" ]; then + cp -f "$BACKUP_LUA_MONITORS" "$LUA_USER_DIR/monitors.lua" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - Restored file: ${MAGENTA:-}UserConfigs/monitors.lua${RESET:-}" 2>&1 | tee -a "$log" fi - done + if [ -n "$BACKUP_LUA_WORKSPACES" ]; then + cp -f "$BACKUP_LUA_WORKSPACES" "$LUA_USER_DIR/workspaces.lua" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - Restored file: ${MAGENTA:-}UserConfigs/workspaces.lua${RESET:-}" 2>&1 | tee -a "$log" + fi + else + local FILE_B=("monitors.conf" "workspaces.conf") + for FILE_RESTORE in "${FILE_B[@]}"; do + local BACKUP_FILE="$BACKUP_HYPR_PATH/$FILE_RESTORE" + if [ -f "$BACKUP_FILE" ]; then + cp "$BACKUP_FILE" "$HYPR_DIR/$FILE_RESTORE" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - Restored file: ${MAGENTA:-}$FILE_RESTORE${RESET:-}" 2>&1 | tee -a "$log" + fi + done + fi fi } @@ -348,20 +388,27 @@ restore_user_configs() { local log="$1" local express_mode="$2" local old_version="$3" - if [ "${RUN_MODE:-}" = "install" ]; then - return - fi local DIRPATH="$HOME/.config/hypr" local BACKUP_DIR BACKUP_DIR=$(get_backup_dirname) local BACKUP_DIR_PATH="$DIRPATH-backup-$BACKUP_DIR/UserConfigs" + local BACKUP_CONFIGS_PATH="$DIRPATH-backup-$BACKUP_DIR/configs" if [ -z "$BACKUP_DIR" ]; then echo "${ERROR:-[ERROR]} - Backup directory name is empty. Exiting." 2>&1 | tee -a "$log" exit 1 fi + if [ "${RUN_MODE:-}" = "install" ]; then + if [ -d "$BACKUP_DIR_PATH" ]; then + echo "${NOTE:-[NOTE]} Preserving existing UserConfigs directory during install." 2>&1 | tee -a "$log" + rsync -a "$BACKUP_DIR_PATH/" "$DIRPATH/UserConfigs/" 2>&1 | tee -a "$log" + echo "${OK:-[OK]} - UserConfigs directory preserved." 2>&1 | tee -a "$log" + fi + return + fi + if [ -d "$BACKUP_DIR_PATH" ]; then local VERSION_FILE VERSION_FILE=$(find "$DIRPATH" -maxdepth 1 -name "v*.*.*" | head -n 1) @@ -409,11 +456,13 @@ restore_user_configs() { "ENVariables.conf" "LaptopDisplay.conf" "Laptops.conf" + "monitors.lua" "Startup_Apps.conf" "UserDecorations.conf" "UserAnimations.conf" "UserKeybinds.conf" "UserSettings.conf" + "workspaces.lua" "WindowRules.conf" ) @@ -455,6 +504,19 @@ restore_user_configs() { fi fi + if [ -d "$BACKUP_CONFIGS_PATH" ]; then + local restored_system_lua=0 + local lua_file + mkdir -p "$DIRPATH/configs" + while IFS= read -r -d '' lua_file; do + cp -f "$lua_file" "$DIRPATH/configs/" + restored_system_lua=1 + done < <(find "$BACKUP_CONFIGS_PATH" -maxdepth 1 -type f -name 'system_*.lua' -print0) + if [ "$restored_system_lua" -eq 1 ]; then + echo "${OK:-[OK]} - Restored migrated system Lua overlays to $DIRPATH/configs." 2>&1 | tee -a "$log" + fi + fi + # Always run de-dupe based on the installed dotfiles version so that # express mode and standard mode behave consistently. Prefer the # pre-upgrade version (old_version) if provided so we still clean up diff --git a/scripts/migrate-hypr-to-lua.sh b/scripts/migrate-hypr-to-lua.sh new file mode 100755 index 00000000..ccbd6ed9 --- /dev/null +++ b/scripts/migrate-hypr-to-lua.sh @@ -0,0 +1,2212 @@ +#!/usr/bin/env bash +# ================================================== +# KoolDots (2026) +# Project URL: https://github.com/LinuxBeginnings +# License: GNU GPLv3 +# SPDX-License-Identifier: GPL-3.0-or-later +# ================================================== +# Opt-in helper for testing Hyprland's Lua config entrypoint. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +SRC_HYPR_DIR="$REPO_DIR/config/hypr" +DEST_HYPR_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/hypr" +BACKUP_DIR="${DEST_HYPR_DIR}-backup-lua-$(date +%Y%m%d-%H%M%S)" +MIGRATION_TS="$(date +%Y%m%d-%H%M%S)" +DRY_RUN=0 +YES=0 +REVERT=0 + +USER_CONFIGS_DIR="$DEST_HYPR_DIR/UserConfigs" +CONFIGS_DIR="$DEST_HYPR_DIR/configs" +LEGACY_CONFIGS_DIR_NAME="LegacyConfigs" +USER_WINDOW_RULES="$USER_CONFIGS_DIR/WindowRules.conf" +USER_KEYBINDS="$USER_CONFIGS_DIR/UserKeybinds.conf" +USER_ENV_VARS="$USER_CONFIGS_DIR/ENVariables.conf" +USER_STARTUP_APPS="$USER_CONFIGS_DIR/Startup_Apps.conf" +USER_SETTINGS="$USER_CONFIGS_DIR/UserSettings.conf" +USER_DECORATIONS="$USER_CONFIGS_DIR/UserDecorations.conf" +USER_ANIMATIONS="$USER_CONFIGS_DIR/UserAnimations.conf" +USER_LAPTOPS="$USER_CONFIGS_DIR/Laptops.conf" +USER_LAYER_RULES="$USER_CONFIGS_DIR/LayerRules.conf" +SYSTEM_WINDOW_RULES="$CONFIGS_DIR/WindowRules.conf" +SYSTEM_LAYER_RULES="$CONFIGS_DIR/LayerRules.conf" +SYSTEM_KEYBINDS="$CONFIGS_DIR/Keybinds.conf" +SYSTEM_ENV_VARS="$CONFIGS_DIR/ENVariables.conf" +SYSTEM_STARTUP_APPS="$CONFIGS_DIR/Startup_Apps.conf" +SYSTEM_SETTINGS="$CONFIGS_DIR/SystemSettings.conf" +SYSTEM_LAPTOPS="$CONFIGS_DIR/Laptops.conf" +USER_CONFIGS_LEGACY_ROOT="$USER_CONFIGS_DIR/$LEGACY_CONFIGS_DIR_NAME" +CONFIGS_LEGACY_ROOT="$CONFIGS_DIR/$LEGACY_CONFIGS_DIR_NAME" +USER_CONFIGS_LEGACY_DIR="$USER_CONFIGS_LEGACY_ROOT/$MIGRATION_TS" +CONFIGS_LEGACY_DIR="$CONFIGS_LEGACY_ROOT/$MIGRATION_TS" +USER_OVERRIDES_SHIM="$DEST_HYPR_DIR/lua/user_overrides.lua" +DEST_MONITORS_CONF="$DEST_HYPR_DIR/monitors.conf" +DEST_LUA_MONITORS="$DEST_HYPR_DIR/lua/monitors.lua" +DEST_WORKSPACES_CONF="$DEST_HYPR_DIR/workspaces.conf" +DEST_LUA_WORKSPACES="$DEST_HYPR_DIR/lua/workspaces.lua" +SOURCE_LUA_ENTRY_ENABLED="$SRC_HYPR_DIR/hyprland.lua" +SOURCE_LUA_ENTRY_DISABLED="$SRC_HYPR_DIR/hyprland.lua.disable" +DEST_LUA_ENTRY="$DEST_HYPR_DIR/hyprland.lua" +DEST_LUA_ENTRY_DISABLED="$DEST_HYPR_DIR/hyprland.lua.disable" +SOURCE_LUA_ENTRY="" + +usage() { + cat <<USAGE +Usage: $(basename "$0") [--yes] [--dry-run] [--revert] + +Copies the repo's Hyprland Lua entrypoint into: + $DEST_HYPR_DIR + +This preserves hyprland.conf as fallback and creates a full backup of the +current Hyprland config directory before changing files. + +Options: + -y, --yes Run without confirmation prompts. + -n, --dry-run Show what would change without copying files. + -r, --revert Revert migration by restoring latest LegacyConfigs/<timestamp> .conf files. + -h, --help Show this help. +USAGE +} + +while [ "$#" -gt 0 ]; do + case "$1" in + -y|--yes) + YES=1 + ;; + -n|--dry-run) + DRY_RUN=1 + ;; + -r|--revert) + REVERT=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "[ERROR] Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac + shift +done + +if [ -f "$SOURCE_LUA_ENTRY_ENABLED" ]; then + SOURCE_LUA_ENTRY="$SOURCE_LUA_ENTRY_ENABLED" +elif [ -f "$SOURCE_LUA_ENTRY_DISABLED" ]; then + SOURCE_LUA_ENTRY="$SOURCE_LUA_ENTRY_DISABLED" +fi + +if [ "$REVERT" -eq 0 ] && [ ! -d "$SRC_HYPR_DIR/lua" ]; then + echo "[ERROR] Lua config source files were not found under $SRC_HYPR_DIR" >&2 + exit 1 +fi + +if [ "$REVERT" -eq 0 ] && [ -z "$SOURCE_LUA_ENTRY" ] && [ ! -f "$DEST_LUA_ENTRY" ] && [ ! -f "$DEST_LUA_ENTRY_DISABLED" ]; then + echo "[ERROR] No Lua entrypoint was found at $SRC_HYPR_DIR/hyprland.lua(.disable) or $DEST_HYPR_DIR/hyprland.lua(.disable)" >&2 + exit 1 +fi + +if command -v Hyprland >/dev/null 2>&1; then + HYPR_VERSION="$(Hyprland --version 2>/dev/null | sed -n '1p' || true)" + echo "[INFO] Detected $HYPR_VERSION" +else + echo "[WARN] Hyprland binary was not found in PATH; continuing because this may be an offline config migration." +fi + +echo "[INFO] Source: $SRC_HYPR_DIR" +echo "[INFO] Target: $DEST_HYPR_DIR" +echo "[INFO] Backup: $BACKUP_DIR" +if [ "$REVERT" -eq 1 ]; then + echo "[WARN] Revert mode: restores latest LegacyConfigs/<timestamp> .conf files in UserConfigs and configs." +else + echo "[WARN] This enables Hyprland's Lua entrypoint for builds that support hyprland.lua." + echo "[WARN] hyprland.conf remains in place as fallback; old .conf files move into LegacyConfigs/<timestamp>." + echo "[INFO] hypridle.conf and hyprlock*.conf stay as native .conf files because Hypridle/Hyprlock do not use Hyprland's Lua API." +fi + +restore_latest_conf_backup() { + local target_dir="$1" + local label="$2" + local latest_archive="" + local legacy_root="$target_dir/$LEGACY_CONFIGS_DIR_NAME" + local moved=0 + local file + local archives=() + + [ -d "$target_dir" ] || return 0 + + if [ -d "$legacy_root" ]; then + mapfile -t archives < <(find "$legacy_root" -mindepth 1 -maxdepth 1 -type d | sort) + fi + + if [ "${#archives[@]}" -eq 0 ]; then + mapfile -t archives < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type d -name 'backup-*' | sort) + fi + + [ "${#archives[@]}" -gt 0 ] || return 0 + latest_archive="${archives[${#archives[@]}-1]}" + + while IFS= read -r -d '' file; do + cp -a "$file" "$target_dir/" + moved=1 + done < <(find "$latest_archive" -maxdepth 1 -type f -name '*.conf' -print0) + + if [ "$moved" -eq 1 ]; then + echo "[OK] Restored $label/*.conf from $latest_archive" + else + echo "[INFO] No .conf files found in latest archive for $label: $latest_archive" + fi +} + +if [ "$YES" -eq 0 ]; then + if [ "$REVERT" -eq 1 ]; then + printf "[ACTION] Continue and revert Lua migration using latest legacy archives? [y/N] " + else + printf "[ACTION] Continue with Lua config migration? [y/N] " + fi + read -r reply + case "$reply" in + [Yy]|[Yy][Ee][Ss]) + ;; + *) + echo "[INFO] Cancelled. No changes made." + exit 0 + ;; + esac +fi + +if [ "$DRY_RUN" -eq 1 ]; then + if [ "$REVERT" -eq 1 ]; then + if [ -f "$DEST_LUA_ENTRY" ]; then + echo "[DRY-RUN] Would disable Lua entrypoint: $DEST_LUA_ENTRY -> $DEST_LUA_ENTRY_DISABLED" + elif [ -f "$DEST_LUA_ENTRY_DISABLED" ]; then + echo "[DRY-RUN] Lua entrypoint already disabled: $DEST_LUA_ENTRY_DISABLED" + else + echo "[DRY-RUN] No Lua entrypoint found to disable at: $DEST_LUA_ENTRY" + fi + echo "[DRY-RUN] Would restore latest LegacyConfigs/<timestamp> .conf files into:" + echo "[DRY-RUN] - $USER_CONFIGS_DIR" + echo "[DRY-RUN] - $CONFIGS_DIR" + else + echo "[DRY-RUN] Would create target directory if missing: $DEST_HYPR_DIR" + if [ -d "$DEST_HYPR_DIR" ]; then + echo "[DRY-RUN] Would copy backup: $DEST_HYPR_DIR -> $BACKUP_DIR" + fi + if [ -f "$DEST_LUA_ENTRY_DISABLED" ]; then + echo "[DRY-RUN] Would enable Lua entrypoint: $DEST_LUA_ENTRY_DISABLED -> $DEST_LUA_ENTRY" + elif [ -f "$DEST_LUA_ENTRY" ]; then + echo "[DRY-RUN] Lua entrypoint already enabled: $DEST_LUA_ENTRY" + elif [ -n "$SOURCE_LUA_ENTRY" ]; then + echo "[DRY-RUN] Would install Lua entrypoint: $SOURCE_LUA_ENTRY -> $DEST_LUA_ENTRY" + fi + echo "[DRY-RUN] Would replace Lua module directory: $DEST_HYPR_DIR/lua" + echo "[DRY-RUN] Would generate split configs/UserConfigs Lua overlays:" + echo "[DRY-RUN] - $CONFIGS_DIR/system_env.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_startup.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_window_rules.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_layer_rules.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_keybinds.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_settings.lua" + echo "[DRY-RUN] - $CONFIGS_DIR/system_laptops.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_env.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_startup.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_window_rules.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_layer_rules.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_keybinds.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_settings.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_decorations.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_animations.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_laptops.lua" + echo "[DRY-RUN] - $USER_CONFIGS_DIR/user_defaults.lua" + echo "[DRY-RUN] - $DEST_LUA_MONITORS (generated from $DEST_MONITORS_CONF)" + echo "[DRY-RUN] - $DEST_LUA_WORKSPACES (generated from $DEST_WORKSPACES_CONF)" + if [ -d "$USER_CONFIGS_DIR" ]; then + echo "[DRY-RUN] Would move UserConfigs/*.conf into: $USER_CONFIGS_LEGACY_DIR" + fi + if [ -d "$CONFIGS_DIR" ]; then + echo "[DRY-RUN] Would move configs/*.conf into: $CONFIGS_LEGACY_DIR" + fi + fi + exit 0 +fi +if [ "$REVERT" -eq 1 ]; then + if [ -f "$DEST_LUA_ENTRY" ]; then + mv -f "$DEST_LUA_ENTRY" "$DEST_LUA_ENTRY_DISABLED" + echo "[OK] Disabled Lua entrypoint: $DEST_LUA_ENTRY_DISABLED" + elif [ -f "$DEST_LUA_ENTRY_DISABLED" ]; then + echo "[INFO] Lua entrypoint already disabled: $DEST_LUA_ENTRY_DISABLED" + else + echo "[INFO] No Lua entrypoint found to disable at $DEST_LUA_ENTRY" + fi + restore_latest_conf_backup "$USER_CONFIGS_DIR" "$USER_CONFIGS_DIR" + restore_latest_conf_backup "$CONFIGS_DIR" "$CONFIGS_DIR" + echo "[OK] Revert complete." + echo "[INFO] Restart Hyprland to load restored .conf files." + exit 0 +fi + +mkdir -p "$DEST_HYPR_DIR" + +if [ -d "$DEST_HYPR_DIR" ] && [ -n "$(find "$DEST_HYPR_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + cp -a "$DEST_HYPR_DIR" "$BACKUP_DIR" + echo "[OK] Backup created at $BACKUP_DIR" +fi +if [ -f "$DEST_LUA_ENTRY_DISABLED" ]; then + if [ -f "$DEST_LUA_ENTRY" ]; then + rm -f "$DEST_LUA_ENTRY_DISABLED" + echo "[INFO] Removed duplicate disabled entrypoint: $DEST_LUA_ENTRY_DISABLED" + else + mv "$DEST_LUA_ENTRY_DISABLED" "$DEST_LUA_ENTRY" + echo "[OK] Enabled Lua entrypoint: $DEST_LUA_ENTRY" + fi +elif [ -f "$DEST_LUA_ENTRY" ]; then + echo "[INFO] Lua entrypoint already enabled: $DEST_LUA_ENTRY" +elif [ -n "$SOURCE_LUA_ENTRY" ]; then + cp -f "$SOURCE_LUA_ENTRY" "$DEST_LUA_ENTRY" + echo "[OK] Installed Lua entrypoint: $SOURCE_LUA_ENTRY -> $DEST_LUA_ENTRY" +else + echo "[ERROR] Unable to locate a Lua entrypoint to enable." >&2 + exit 1 +fi +rm -rf "$DEST_HYPR_DIR/lua" +cp -a "$SRC_HYPR_DIR/lua" "$DEST_HYPR_DIR/lua" +mkdir -p "$USER_CONFIGS_DIR" "$CONFIGS_DIR" +python3 - \ + "$CONFIGS_DIR" \ + "$USER_CONFIGS_DIR" \ + "$SYSTEM_WINDOW_RULES" \ + "$SYSTEM_LAYER_RULES" \ + "$SYSTEM_KEYBINDS" \ + "$SYSTEM_ENV_VARS" \ + "$SYSTEM_STARTUP_APPS" \ + "$SYSTEM_SETTINGS" \ + "$SYSTEM_LAPTOPS" \ + "$USER_WINDOW_RULES" \ + "$USER_LAYER_RULES" \ + "$USER_KEYBINDS" \ + "$USER_ENV_VARS" \ + "$USER_STARTUP_APPS" \ + "$USER_SETTINGS" \ + "$USER_DECORATIONS" \ + "$USER_ANIMATIONS" \ + "$USER_LAPTOPS" \ + "$USER_CONFIGS_DIR/01-UserDefaults.conf" \ + "$DEST_MONITORS_CONF" \ + "$DEST_LUA_MONITORS" \ + "$DEST_WORKSPACES_CONF" \ + "$DEST_LUA_WORKSPACES" <<'PY' +import os +import re +import sys +from pathlib import Path +HEADER = """-- ================================================== +-- KoolDots (2026) +-- Project URL: https://github.com/LinuxBeginnings +-- License: GNU GPLv3 +-- SPDX-License-Identifier: GPL-3.0-or-later +-- ================================================== +""" +system_configs_dir = Path(sys.argv[1]) +user_configs_dir = Path(sys.argv[2]) +system_window_rules_path = Path(sys.argv[3]) +system_layer_rules_path = Path(sys.argv[4]) +system_keybinds_path = Path(sys.argv[5]) +system_env_path = Path(sys.argv[6]) +system_startup_path = Path(sys.argv[7]) +system_settings_path = Path(sys.argv[8]) +system_laptops_path = Path(sys.argv[9]) +window_rules_path = Path(sys.argv[10]) +layer_rules_path = Path(sys.argv[11]) +keybinds_path = Path(sys.argv[12]) +env_path = Path(sys.argv[13]) +startup_path = Path(sys.argv[14]) +settings_path = Path(sys.argv[15]) +decorations_path = Path(sys.argv[16]) +animations_path = Path(sys.argv[17]) +laptops_path = Path(sys.argv[18]) +user_defaults_path = Path(sys.argv[19]) +monitors_conf_path = Path(sys.argv[20]) +monitors_lua_path = Path(sys.argv[21]) +workspaces_conf_path = Path(sys.argv[22]) +workspaces_lua_path = Path(sys.argv[23]) + +files_out = { + "system_env": system_configs_dir / "system_env.lua", + "system_startup": system_configs_dir / "system_startup.lua", + "system_window_rules": system_configs_dir / "system_window_rules.lua", + "system_layer_rules": system_configs_dir / "system_layer_rules.lua", + "system_keybinds": system_configs_dir / "system_keybinds.lua", + "system_settings": system_configs_dir / "system_settings.lua", + "system_laptops": system_configs_dir / "system_laptops.lua", + "env": user_configs_dir / "user_env.lua", + "startup": user_configs_dir / "user_startup.lua", + "window_rules": user_configs_dir / "user_window_rules.lua", + "layer_rules": user_configs_dir / "user_layer_rules.lua", + "keybinds": user_configs_dir / "user_keybinds.lua", + "settings": user_configs_dir / "user_settings.lua", + "decorations": user_configs_dir / "user_decorations.lua", + "animations": user_configs_dir / "user_animations.lua", + "laptops": user_configs_dir / "user_laptops.lua", + "user_defaults": user_configs_dir / "user_defaults.lua", + "monitors": monitors_lua_path, + "workspaces": workspaces_lua_path, +} + +def strip_comment(line): + return line.split("#", 1)[0].strip() + +def split_items(value): + return [item.strip() for item in value.split(",") if item.strip()] + +def lua_string(value): + return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"' +def write_file(path, lines): + content = "\n".join(lines).rstrip() + "\n" + if not content.startswith(HEADER): + content = HEADER + "\n" + content.lstrip("\n") + path.write_text(content, encoding="utf-8") + print(f"[OK] Wrote {path}") + +def source_examples(path): + if not path.exists(): + return [] + lines = [] + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + continue + lines.append(f"-- {stripped}") + return lines +def latest_legacy_file(path): + legacy_root = path.parent / "LegacyConfigs" + if not legacy_root.is_dir(): + return None + candidates = [] + for snapshot in sorted(legacy_root.iterdir()): + if not snapshot.is_dir(): + continue + candidate = snapshot / path.name + if candidate.is_file(): + candidates.append(candidate) + return candidates[-1] if candidates else None + +def parse_env(path): + entries = [] + source_path = path if path.exists() else latest_legacy_file(path) + if source_path is None: + return entries + if source_path != path: + print(f"[INFO] {path.name} not found at {path}; using legacy source {source_path}") + for raw in source_path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + comma_match = re.match(r"^env\s*=\s*([^,]+)\s*,\s*(.+)$", line) + if comma_match: + entries.append((comma_match.group(1).strip(), comma_match.group(2).strip())) + continue + equals_match = re.match(r"^env\s*=\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$", line) + if equals_match: + entries.append((equals_match.group(1).strip(), equals_match.group(2).strip())) + return entries + +def parse_startup(path, *, variables=None, visited=None): + entries = [] + if not path.exists(): + return entries + + if variables is None: + variables = {} + if visited is None: + visited = set() + + try: + resolved = path.resolve() + except FileNotFoundError: + resolved = path + + if resolved in visited: + return [] + visited.add(resolved) + + def expand(value): + for _ in range(8): + new_value = value + for name, var_value in variables.items(): + new_value = new_value.replace(f"${name}", var_value) + if new_value == value: + break + value = new_value + return os.path.expandvars(value) + + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + + source_match = re.match(r"^source\s*=\s*(.+)$", line) + if source_match: + source_value = expand(source_match.group(1).strip()) + source_path = Path(source_value).expanduser() + entries.extend(parse_startup(source_path, variables=variables, visited=visited)) + continue + + variable = re.match(r"^\$([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$", line) + if variable: + variables[variable.group(1)] = expand(variable.group(2).strip()) + continue + + match = re.match(r"^exec(?:-once)?\s*=\s*(.+)$", line) + if match: + entries.append(expand(match.group(1).strip())) + + return entries + +MONITOR_DIRECTIVE_KEYS = { + "mirror", + "bitdepth", + "transform", + "cm", + "icc", + "vrr", + "addreserved", + "reserved", + "supports_hdr", + "supports_wide_color", + "supportshdr", + "supportswidecolor", + "max_luminance", + "max_avg_luminance", + "min_luminance", + "maxluminance", + "maxavgluminance", + "minluminance", + "sdr_max_luminance", + "sdr_min_luminance", + "sdr_eotf", + "sdrmaxluminance", + "sdrminluminance", + "sdreotf", + "sdrbrightness", + "sdrsaturation", +} + +MONITOR_FIELD_MAP = { + "addreserved": "reserved", + "supportswidecolor": "supports_wide_color", + "supportshdr": "supports_hdr", + "maxluminance": "max_luminance", + "maxavgluminance": "max_avg_luminance", + "minluminance": "min_luminance", + "sdrmaxluminance": "sdr_max_luminance", + "sdrminluminance": "sdr_min_luminance", + "sdreotf": "sdr_eotf", +} + +def parse_monitors(path): + entries = [] + if not path.exists(): + return entries + + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^monitor\s*=\s*(.+)$", line) + if not match: + continue + + parts = [part.strip() for part in match.group(1).split(",")] + if len(parts) < 2: + continue + + spec = {"output": parts[0]} + mode_or_directive = parts[1].lower().replace("-", "_") + extras = [] + + if mode_or_directive == "disable": + spec["mode"] = "disable" + entries.append(spec) + continue + + if len(parts) >= 4 and mode_or_directive not in MONITOR_DIRECTIVE_KEYS: + spec["mode"] = parts[1] + if parts[2]: + spec["position"] = parts[2] + if parts[3]: + spec["scale"] = parts[3] + extras = parts[4:] + else: + extras = parts[1:] + + i = 0 + while i < len(extras): + key = extras[i].strip().lower().replace("-", "_") + if not key: + i += 1 + continue + field = MONITOR_FIELD_MAP.get(key, key) + + if field == "reserved" and i + 4 < len(extras): + spec["reserved"] = [ + extras[i + 1].strip(), + extras[i + 2].strip(), + extras[i + 3].strip(), + extras[i + 4].strip(), + ] + i += 5 + continue + + if i + 1 >= len(extras): + i += 1 + continue + + value = extras[i + 1].strip() + if value: + spec[field] = value + i += 2 + + entries.append(spec) + + return entries +def parse_bool_word(value): + lowered = value.strip().lower() + if lowered in {"on", "true", "yes"}: + return True + if lowered in {"off", "false", "no"}: + return False + return None + +WORKSPACE_FIELD_MAP = { + "gapsin": "gaps_in", + "gapsout": "gaps_out", + "bordersize": "border_size", + "on_created_empty": "on_created_empty", + "layoutopt": "layout_opts", + "layoutopts": "layout_opts", +} + +WORKSPACE_INVERTED_BOOL_FIELDS = { + "border": "no_border", + "rounding": "no_rounding", + "shadow": "no_shadow", +} + +def parse_workspaces(path): + entries = [] + if not path.exists(): + return entries + + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^workspace\s*=\s*(.+)$", line) + if not match: + continue + + parts = [part.strip() for part in match.group(1).split(",")] + if not parts or not parts[0]: + continue + + rule = {"workspace": parts[0]} + for part in parts[1:]: + if ":" not in part: + continue + key, value = part.split(":", 1) + key = key.strip().lower().replace("-", "_") + value = value.strip() + if not key or value == "": + continue + + if key in WORKSPACE_INVERTED_BOOL_FIELDS: + bool_value = parse_bool_word(value) + if bool_value is None: + continue + rule[WORKSPACE_INVERTED_BOOL_FIELDS[key]] = not bool_value + continue + + normalized = WORKSPACE_FIELD_MAP.get(key, key) + bool_value = parse_bool_word(value) + if bool_value is not None: + rule[normalized] = bool_value + else: + rule[normalized] = value + + entries.append(rule) + + return entries + +def emit_monitor(spec): + lines = [ + "hl.monitor({", + f" output = {lua_string(spec.get('output', ''))},", + ] + + if "mode" in spec: + lines.append(f" mode = {lua_string(spec['mode'])},") + if "position" in spec: + lines.append(f" position = {lua_string(spec['position'])},") + if "scale" in spec: + lines.append(f" scale = {lua_string(spec['scale'])},") + if "mirror" in spec: + lines.append(f" mirror = {lua_string(spec['mirror'])},") + + for key in [ + "bitdepth", + "transform", + "vrr", + "supports_hdr", + "supports_wide_color", + "max_luminance", + "max_avg_luminance", + "min_luminance", + "sdr_max_luminance", + "sdr_min_luminance", + "sdrbrightness", + "sdrsaturation", + ]: + if key in spec: + lines.append(f" {key} = {scalar(spec[key])},") + + for key in ["cm", "icc", "sdr_eotf"]: + if key in spec: + lines.append(f" {key} = {lua_string(spec[key])},") + + if "reserved" in spec and len(spec["reserved"]) == 4: + top, right, bottom, left = spec["reserved"] + lines.extend([ + " reserved = {", + f" top = {scalar(top)},", + f" right = {scalar(right)},", + f" bottom = {scalar(bottom)},", + f" left = {scalar(left)},", + " },", + ]) + + lines.append("})") + return "\n".join(lines) + +def emit_workspace_rule(spec): + lines = [ + "hl.workspace_rule({", + f" workspace = {lua_string(spec['workspace'])},", + ] + for key, value in spec.items(): + if key == "workspace": + continue + if isinstance(value, bool): + rendered = "true" if value else "false" + else: + text = str(value).strip() + if re.fullmatch(r"[-+]?\d+(\.\d+)?", text): + rendered = text + else: + rendered = lua_string(text) + lines.append(f" {key} = {rendered},") + lines.append("})") + return "\n".join(lines) +def unquote(value): + value = value.strip() + if len(value) >= 2 and ( + (value[0] == "\"" and value[-1] == "\"") + or (value[0] == "'" and value[-1] == "'") + ): + return value[1:-1] + return value + +def resolve_shell_default(value): + value = value.strip() + match = re.fullmatch(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\:-([^}]*)\}", value) + if match: + env_value = os.getenv(match.group(1), "") + return env_value if env_value else match.group(2) + match = re.fullmatch(r"\$\{([A-Za-z_][A-Za-z0-9_]*)-([^}]*)\}", value) + if match: + env_value = os.getenv(match.group(1)) + return env_value if env_value is not None else match.group(2) + match = re.fullmatch(r"\$([A-Za-z_][A-Za-z0-9_]*)", value) + if match: + return os.getenv(match.group(1), "") + return value + +def parse_user_defaults(path): + defaults = {} + if not path.exists(): + return defaults + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^\$([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$", line) + if not match: + continue + key = match.group(1) + value = unquote(resolve_shell_default(match.group(2))) + defaults[key] = value + return defaults + +def parse_hyprlang_sections(path): + sections = {} + if path is None or not path.exists(): + return sections + + stack = [sections] + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + if line.startswith("source"): + continue + if line.endswith("{"): + name = line[:-1].strip().replace("-", "_") + if not name or name.startswith("$"): + continue + current = stack[-1] + target = current.setdefault(name, {}) + stack.append(target) + continue + if line == "}": + if len(stack) > 1: + stack.pop() + continue + if "=" in line: + key, value = [part.strip() for part in line.split("=", 1)] + if not key or key.startswith("$"): + continue + key = key.replace("-", "_") + container = stack[-1] + parts = [part.replace("-", "_") for part in key.split(".") if part] + for part in parts[:-1]: + container = container.setdefault(part, {}) + container[parts[-1]] = value + return sections + +def parse_scripts_dir(path): + if path is None or not path.exists(): + return None + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^\\$scriptsDir\\s*=\\s*(.+)$", line) + if match: + return unquote(match.group(1).strip()) + return None + +def parse_gestures(path): + simple = [] + complex_entries = [] + if path is None or not path.exists(): + return simple, complex_entries + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw) + if not line: + continue + match = re.match(r"^gesture\\s*=\\s*(.+)$", line) + if not match: + continue + payload = match.group(1).strip() + parts = [part.strip() for part in payload.split(",")] + if len(parts) >= 3 and parts[2] == "workspace": + try: + fingers = int(parts[0]) + except ValueError: + complex_entries.append(payload) + continue + simple.append({ + "fingers": fingers, + "direction": parts[1], + "action": "workspace", + }) + else: + complex_entries.append(payload) + return simple, complex_entries + +WALLUST_FALLBACKS = { + "color12": "rgba(8db4ffff)", + "color10": "rgba(5f6578ff)", + "color15": "rgba(ffffffff)", + "color0": "rgba(0f111aff)", +} + +def wallust_expr(value): + value = value.strip() + if not value.startswith("$"): + return None + var = value[1:].strip().lower() + if re.fullmatch(r"color\d+", var): + fallback = WALLUST_FALLBACKS.get(var) + if fallback: + return f"wallust_color({lua_string(var)}, {lua_string(fallback)})" + return f"wallust_color({lua_string(var)})" + return None + +def lua_value(value): + wallust_value = wallust_expr(value) + if wallust_value is not None: + return wallust_value + return scalar(value) + +def render_table(value, indent=4): + lines = [] + for key, child in value.items(): + if isinstance(child, dict): + lines.append(" " * indent + f"{key} = {{") + lines.extend(render_table(child, indent + 2)) + lines.append(" " * indent + "},") + else: + rendered = lua_value(str(child)) + if rendered is None: + continue + lines.append(" " * indent + f"{key} = {rendered},") + return lines + +def scalar(value, *, bool_words=True): + value = value.strip() + lower = value.lower() + if bool_words and lower in {"on", "true", "yes"}: + return "true" + if bool_words and lower in {"off", "false", "no"}: + return "false" + if re.fullmatch(r"[-+]?\d+(\.\d+)?", value): + return value + return lua_string(value) + +def normalize_field(name): + aliases = { + "floating": "float", + "pinned": "pin", + "fullscreenstate": "fullscreen_state", + "initialclass": "initial_class", + "initialtitle": "initial_title", + "xdgtag": "xdg_tag", + "onworkspace": "workspace", + "ignorealpha": "ignore_alpha", + "ignorezero": "ignore_zero", + "noanim": "no_anim", + "noblur": "no_blur", + "noshadow": "no_shadow", + "nofocus": "no_focus", + "noinitialfocus": "no_initial_focus", + "keepaspectratio": "keep_aspect_ratio", + "idleinhibit": "idle_inhibit", + "bordersize": "border_size", + "bordercolor": "border_color", + "roundingpower": "rounding_power", + "allowsinput": "allows_input", + "dimaround": "dim_around", + "focusonactivate": "focus_on_activate", + "nearestneighbor": "nearest_neighbor", + "nofollowmouse": "no_follow_mouse", + "noscreenshare": "no_screen_share", + "novrr": "no_vrr", + "forcergbx": "force_rgbx", + "suppressevent": "suppress_event", + "maxsize": "max_size", + "minsize": "min_size", + "persistentsize": "persistent_size", + "nomaxsize": "no_max_size", + } + compact = name.strip().replace("-", "_") + return aliases.get(compact.lower(), compact) + +MATCH_BOOL_FIELDS = { + "xwayland", + "float", + "fullscreen", + "pin", + "focus", + "group", + "modal", +} + +def parse_rule_item(item, rule): + if item.startswith("match:"): + body = item[len("match:"):].strip() + if "=" in body: + key, value = body.split("=", 1) + else: + parts = body.split(None, 1) + if len(parts) != 2: + return + key, value = parts + key = normalize_field(key) + rule.setdefault("match", {})[key] = scalar(value, bool_words=key in MATCH_BOOL_FIELDS) + return + + parts = item.split(None, 1) + key = normalize_field(parts[0]) + value = parts[1] if len(parts) > 1 else "on" + rule[key] = scalar(value) + +def parse_block(lines, start_index): + rule_type = "window" if lines[start_index].strip().startswith("windowrule") else "layer" + rule = {"match": {}} + i = start_index + 1 + while i < len(lines): + line = strip_comment(lines[i]) + if line == "}": + break + if line: + if "=" in line: + key, value = [part.strip() for part in line.split("=", 1)] + if key.startswith("match:"): + match_key = normalize_field(key[len("match:"):]) + rule["match"][match_key] = scalar(value, bool_words=match_key in MATCH_BOOL_FIELDS) + elif key == "name": + rule["name"] = lua_string(value) + else: + rule[normalize_field(key)] = scalar(value) + i += 1 + return rule_type, rule, i + +def parse_rules(path, prefix): + if not path.exists(): + return [] + + parsed = [] + lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() + i = 0 + rule_index = 1 + layer_index = 1 + pending_name = None + while i < len(lines): + raw_line = lines[i] + name_hint = re.match(r"^\s*#\s*name\s*[:=]\s*(.+?)\s*$", raw_line) + if name_hint: + pending_name = lua_string(name_hint.group(1).strip()) + i += 1 + continue + + line = strip_comment(raw_line) + if not line: + i += 1 + continue + + if re.match(r"^(windowrule|layerrule)\s*\{", line): + rule_type, rule, i = parse_block(lines, i) + if rule.get("match"): + if "name" not in rule: + if pending_name is not None: + rule["name"] = pending_name + elif rule_type == "window": + rule["name"] = lua_string(f"{prefix}-windowrule-{rule_index:03d}") + rule_index += 1 + else: + rule["name"] = lua_string(f"{prefix}-layerrule-{layer_index:03d}") + layer_index += 1 + pending_name = None + parsed.append((rule_type, rule)) + i += 1 + continue + + match = re.match(r"^(windowrule|layerrule)\s*=\s*(.+)$", line) + if match: + rule_type = "window" if match.group(1) == "windowrule" else "layer" + rule = {"match": {}} + for item in split_items(match.group(2)): + parse_rule_item(item, rule) + if rule.get("match"): + if "name" not in rule: + if pending_name is not None: + rule["name"] = pending_name + elif rule_type == "window": + rule["name"] = lua_string(f"{prefix}-windowrule-{rule_index:03d}") + rule_index += 1 + else: + rule["name"] = lua_string(f"{prefix}-layerrule-{layer_index:03d}") + layer_index += 1 + pending_name = None + parsed.append((rule_type, rule)) + else: + pending_name = None + i += 1 + continue + + pending_name = None + i += 1 + return parsed + +def emit_rule(rule_type, rule): + fn = "apply_window_rule" if rule_type == "window" else "apply_layer_rule" + lines = [f"{fn}({{"] + if "name" in rule: + lines.append(f" name = {rule['name']},") + if rule.get("match"): + lines.append(" match = {") + for key, value in rule["match"].items(): + lines.append(f" {key} = {value},") + lines.append(" },") + for key, value in rule.items(): + if key in {"name", "match"}: + continue + lines.append(f" {key} = {value},") + lines.append("})") + return "\n".join(lines) + +def parse_keybinds(path, *, variables=None, visited=None): + if not path.exists(): + return [] + + if variables is None: + variables = {} + if visited is None: + visited = set() + + try: + resolved = path.resolve() + except FileNotFoundError: + resolved = path + + if resolved in visited: + return [] + visited.add(resolved) + + converted = [] + + def expand(value): + for _ in range(8): + new_value = value + for name, var_value in variables.items(): + new_value = new_value.replace(f"${name}", var_value) + if new_value == value: + return new_value + value = new_value + return value + + for raw_line in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + line = strip_comment(raw_line) + if not line: + continue + + source_match = re.match(r"^source\s*=\s*(.+)$", line) + if source_match: + source_value = expand(source_match.group(1).strip()) + source_value = os.path.expandvars(source_value) + source_path = Path(source_value).expanduser() + converted.extend(parse_keybinds(source_path, variables=variables, visited=visited)) + continue + + variable = re.match(r"^\$([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+)$", line) + if variable: + variables[variable.group(1)] = expand(variable.group(2).strip()) + continue + + unbind = re.match(r"^unbind\s*=\s*(.+)$", line) + if unbind: + parts = [expand(part.strip()) for part in unbind.group(1).split(",")] + if len(parts) >= 2: + converted.append(f"unbind({lua_string(parts[0])}, {lua_string(parts[1])})") + continue + + bind = re.match(r"^(bind[a-z]*)\s*=\s*(.+)$", line) + if bind: + binder = bind.group(1) + parts = [expand(part.strip()) for part in bind.group(2).split(",")] + has_description = "d" in binder and binder != "bind" + description = "" + if has_description and len(parts) >= 4: + mods, key = parts[0], parts[1] + description = parts[2] + dispatcher = parts[3] + args = ", ".join(part for part in parts[4:] if part) + elif len(parts) >= 3: + mods, key = parts[0], parts[1] + dispatcher = parts[2] + args = ", ".join(part for part in parts[3:] if part) + else: + continue + + opts = [] + if description: + opts.append(f"description = {lua_string(description)}") + if "l" in binder: + opts.append("locked = true") + if "e" in binder or "r" in binder: + opts.append("[\"repeat\"] = true") + opts_text = ", { " + ", ".join(opts) + " }" if opts else "" + + if dispatcher == "exec": + converted.append(f"bind({lua_string(mods)}, {lua_string(key)}, exec_cmd({lua_string(args)}){opts_text})") + else: + converted.append(f"bind({lua_string(mods)}, {lua_string(key)}, dispatch({lua_string(dispatcher)}, {lua_string(args)}){opts_text})") + + return converted + +system_window_rules = [rule for rule in parse_rules(system_window_rules_path, "system-window") if rule[0] == "window"] +system_layer_rules = [rule for rule in parse_rules(system_layer_rules_path, "system-layer") if rule[0] == "layer"] +window_rules = [rule for rule in parse_rules(window_rules_path, "user-window") if rule[0] == "window"] +layer_rules = [rule for rule in parse_rules(layer_rules_path, "user-layer") if rule[0] == "layer"] +base_keybind_vars = {} +parse_keybinds(user_defaults_path, variables=base_keybind_vars) +system_keybinds = parse_keybinds(system_keybinds_path, variables=dict(base_keybind_vars)) +keybinds = parse_keybinds(keybinds_path, variables=dict(base_keybind_vars)) +system_env_entries = parse_env(system_env_path) +env_entries = parse_env(env_path) +system_startup_entries = parse_startup(system_startup_path, variables=dict(base_keybind_vars)) +startup_entries = parse_startup(startup_path, variables=dict(base_keybind_vars)) +monitor_entries = parse_monitors(monitors_conf_path) +workspace_entries = parse_workspaces(workspaces_conf_path) +parsed_user_defaults = parse_user_defaults(user_defaults_path) +resolved_edit = parsed_user_defaults.get("edit", os.getenv("EDITOR") or "nano") +resolved_visual = parsed_user_defaults.get("visual", os.getenv("VISUAL") or "") +resolved_term = parsed_user_defaults.get("term", "kitty") +resolved_files = parsed_user_defaults.get("files", "thunar") +resolved_search_engine = parsed_user_defaults.get( + "Search_Engine", + parsed_user_defaults.get("search_engine", "https://www.google.com/search?q={}"), +) + +user_defaults_lines = [ + "-- User defaults overrides (auto-generated).", + "-- Edit this file for terminal/editor/file-manager/search defaults in Lua mode.", + "-- Example:", + "-- KOOLDOTS_DEFAULTS.term = \"ghostty\"", + "", + "KOOLDOTS_DEFAULTS = KOOLDOTS_DEFAULTS or {}", + f"KOOLDOTS_DEFAULTS.edit = {lua_string(resolved_edit)}", + f"KOOLDOTS_DEFAULTS.visual = {lua_string(resolved_visual)}", + f"KOOLDOTS_DEFAULTS.term = {lua_string(resolved_term)}", + f"KOOLDOTS_DEFAULTS.files = {lua_string(resolved_files)}", + f"KOOLDOTS_DEFAULTS.search_engine = {lua_string(resolved_search_engine)}", + f"KOOLDOTS_DEFAULTS.Search_Engine = {lua_string(resolved_search_engine)}", +] +write_file(files_out["user_defaults"], user_defaults_lines) + +if monitor_entries: + monitor_lines = [ + "-- Monitors migrated from monitors.conf (auto-generated).", + "-- Edit monitors.conf and rerun scripts/migrate-hypr-to-lua.sh to regenerate this file.", + "", + ] + for spec in monitor_entries: + monitor_lines.append(emit_monitor(spec)) + monitor_lines.append("") + write_file(files_out["monitors"], monitor_lines) +else: + print(f"[INFO] No active monitor entries found in {monitors_conf_path}; keeping existing {files_out['monitors']}") +if workspace_entries: + workspace_lines = [ + "-- Workspace rules migrated from workspaces.conf (auto-generated).", + "-- Edit workspaces.conf and rerun scripts/migrate-hypr-to-lua.sh to regenerate this file.", + "", + ] + for spec in workspace_entries: + workspace_lines.append(emit_workspace_rule(spec)) + workspace_lines.append("") + write_file(files_out["workspaces"], workspace_lines) +else: + print(f"[INFO] No active workspace rules found in {workspaces_conf_path}; keeping existing {files_out['workspaces']}") +system_env_lines = [ + "-- System defaults migrated from configs/ENVariables.conf (auto-generated).", + "-- Edit this file to keep your previous configs/ ENVariables customizations in Lua mode.", + "-- Example:", + "-- hl.env(\"QT_QPA_PLATFORMTHEME\", \"qt6ct\")", + "", +] +if system_env_entries: + system_env_lines.append("-- Converted from configs/ENVariables.conf") + for key, value in system_env_entries: + system_env_lines.append(f"hl.env({lua_string(key)}, {lua_string(value)})") + write_file(files_out["system_env"], system_env_lines) +else: + if files_out["system_env"].exists(): + print(f"[INFO] No active env entries found in {system_env_path}; keeping existing {files_out['system_env']}") + else: + system_env_lines.append("-- No active env entries were found in configs/ENVariables.conf.") + write_file(files_out["system_env"], system_env_lines) + +startup_readiness = ( + "runtime=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}; " + "export XDG_RUNTIME_DIR=\"$runtime\"; " + "for _ in $(seq 1 200); do " + "if [ -n \"$WAYLAND_DISPLAY\" ] && [ -S \"$runtime/$WAYLAND_DISPLAY\" ]; then break; fi; " + "for sock in \"$runtime\"/wayland-[0-9]*; do [ -S \"$sock\" ] || continue; " + "case \"$(basename \"$sock\")\" in *awww*) continue ;; esac; " + "export WAYLAND_DISPLAY=\"$(basename \"$sock\")\"; break 2; done; " + "sleep 0.1; done; " + "if [ -n \"$HYPRLAND_INSTANCE_SIGNATURE\" ]; then " + "hypr_sock=\"$runtime/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket.sock\"; " + "for _ in $(seq 1 200); do [ -S \"$hypr_sock\" ] && break; sleep 0.1; done; fi" +) + +system_startup_lines = [ + "-- System defaults migrated from configs/Startup_Apps.conf (auto-generated).", + "-- Add commands with exec_once(\"your command\")", + "-- Example:", + "-- exec_once(\"swaync\")", + "", + "local session = os.getenv(\"HYPRLAND_INSTANCE_SIGNATURE\") or \"default\"", + "", + "local function shell_quote(value)", + " return \"'\" .. tostring(value):gsub(\"'\", \"'\\\\''\") .. \"'\"", + "end", + "", + "local function exec_once(cmd)", + " local key = cmd:gsub(\"[^%w_.-]\", \"_\"):sub(1, 80)", + " local marker = \"/tmp/hypr-lua-system-exec-once-\" .. session .. \"-\" .. key", + " local log = \"/tmp/hypr-lua-system-startup-\" .. key .. \".log\"", + f" local readiness = {lua_string(startup_readiness)}", + " local inner = readiness .. \"; \" .. cmd", + " local script = \"[ -e \" .. shell_quote(marker) .. \" ] || { touch \" .. shell_quote(marker) .. \" && sh -lc \" .. shell_quote(inner) .. \" >>\" .. shell_quote(log) .. \" 2>&1 & }\"", + " os.execute(\"sh -lc \" .. shell_quote(script))", + "end", + "", +] +if system_startup_entries: + system_startup_lines.append("-- Converted from configs/Startup_Apps.conf") + system_startup_lines.append("local startup_commands = {") + for cmd in system_startup_entries: + system_startup_lines.append(f" {lua_string(cmd)},") + system_startup_lines.extend([ + "}", + "", + "local function run_startup_commands()", + " for _, cmd in ipairs(startup_commands) do", + " exec_once(cmd)", + " end", + "end", + "", + "if hl and hl.on then", + " hl.on(\"hyprland.start\", run_startup_commands)", + "else", + " run_startup_commands()", + "end", + ]) + write_file(files_out["system_startup"], system_startup_lines) +else: + if files_out["system_startup"].exists(): + print(f"[INFO] No active startup entries found in {system_startup_path}; keeping existing {files_out['system_startup']}") + else: + system_startup_lines.append("-- No active startup entries were found in configs/Startup_Apps.conf.") + write_file(files_out["system_startup"], system_startup_lines) + +system_window_lines = [ + "-- System defaults migrated from configs/WindowRules.conf (auto-generated).", + "-- Add additional rules with apply_window_rule({...}).", + "-- Example:", + "-- apply_window_rule({", + "-- name = \"My System Rule\",", + "-- match = { class = \"^pavucontrol$\" },", + "-- float = true,", + "-- })", + "", + "local function apply_window_rule(rule)", + " if hl.window_rule then", + " hl.window_rule(rule)", + " end", + "end", + "", +] +if system_window_rules: + system_window_lines.append("-- Converted from configs/WindowRules.conf") + for rule_type, rule in system_window_rules: + system_window_lines.append(emit_rule(rule_type, rule)) + system_window_lines.append("") +else: + system_window_lines.append("-- No active window rules were found in configs/WindowRules.conf.") +write_file(files_out["system_window_rules"], system_window_lines) + +system_layer_lines = [ + "-- System defaults migrated from configs/LayerRules.conf (auto-generated).", + "-- Add additional rules with apply_layer_rule({...}).", + "-- Example:", + "-- apply_layer_rule({", + "-- name = \"My Layer Rule\",", + "-- match = { namespace = \"rofi\" },", + "-- blur = true,", + "-- })", + "", + "local function apply_layer_rule(rule)", + " if hl.layer_rule then", + " hl.layer_rule(rule)", + " end", + "end", + "", +] +if system_layer_rules: + system_layer_lines.append("-- Converted from configs/LayerRules.conf") + for rule_type, rule in system_layer_rules: + system_layer_lines.append(emit_rule(rule_type, rule)) + system_layer_lines.append("") +else: + system_layer_lines.append("-- No active layer rules were found in configs/LayerRules.conf.") +write_file(files_out["system_layer_rules"], system_layer_lines) + +system_keybind_lines = [ + "-- System defaults migrated from configs/Keybinds.conf (auto-generated).", + "-- Add keybinds with bind(\"MODS\", \"KEY\", fn, opts).", + "-- Example:", + "-- bind(\"SUPER\", \"Z\", exec_cmd(\"thunar\"), { description = \"Open file manager\" })", + "", + "local dsp = hl.dsp or hl", + "local function resolve_cmd(cmd)", + " local defaults = rawget(_G, \"KOOLDOTS_DEFAULTS\") or {}", + " local resolved_term = defaults.term or os.getenv(\"TERMINAL\") or \"kitty\"", + " local resolved_files = defaults.files or \"thunar\"", + " local resolved_edit = defaults.edit or os.getenv(\"EDITOR\") or \"nano\"", + " cmd = tostring(cmd)", + " cmd = cmd:gsub(\"%$term\", resolved_term)", + " cmd = cmd:gsub(\"%$files\", resolved_files)", + " cmd = cmd:gsub(\"%$edit\", resolved_edit)", + " return cmd", + "end", + "", + "local function exec_cmd(cmd)", + " local resolved = resolve_cmd(cmd)", + " if dsp and dsp.exec_cmd then", + " return dsp.exec_cmd(resolved)", + " end", + " return function() hl.exec_cmd(resolved) end", + "end", + "", + "local function shell_quote(value)", + " return \"'\" .. tostring(value):gsub(\"'\", \"'\\\\''\") .. \"'\"", + "end", + "", + "local function raw_dispatch_cmd(command)", + " if dsp and dsp.exec_raw then", + " return dsp.exec_raw(tostring(command))", + " end", + " local expression = \"hl.dsp.exec_raw(\" .. string.format(\"%q\", tostring(command)) .. \")\"", + " return exec_cmd(\"hyprctl dispatch \" .. shell_quote(expression))", + "end", + "", + "local function trim(value)", + " return (value or \"\"):gsub(\"^%s+\", \"\"):gsub(\"%s+$\", \"\")", + "end", + "", + "local function chord(mods, key)", + " mods = trim(mods):gsub(\"%s+\", \" + \")", + " key = trim(key)", + " if mods == \"\" then", + " return key", + " end", + " return mods .. \" + \" .. key", + "end", + "", + "local function key_variants(key, mods)", + " key = trim(key):gsub(\"^xf86\", \"XF86\")", + " local key_aliases = {", + " XF86AudioPlayPause = \"XF86AudioPlay\",", + " XF86audiolowervolume = \"XF86AudioLowerVolume\",", + " XF86audiomute = \"XF86AudioMute\",", + " XF86audioraisevolume = \"XF86AudioRaiseVolume\",", + " XF86audiostop = \"XF86AudioStop\",", + " }", + " key = key_aliases[key] or key", + " local shifted_number_keys = {", + " [\"code:10\"] = \"exclam\",", + " [\"code:11\"] = \"at\",", + " [\"code:12\"] = \"numbersign\",", + " [\"code:13\"] = \"dollar\",", + " [\"code:14\"] = \"percent\",", + " [\"code:15\"] = \"asciicircum\",", + " [\"code:16\"] = \"ampersand\",", + " [\"code:17\"] = \"asterisk\",", + " [\"code:18\"] = \"parenleft\",", + " [\"code:19\"] = \"parenright\",", + " }", + " local number_keys = {", + " [\"code:10\"] = \"1\",", + " [\"code:11\"] = \"2\",", + " [\"code:12\"] = \"3\",", + " [\"code:13\"] = \"4\",", + " [\"code:14\"] = \"5\",", + " [\"code:15\"] = \"6\",", + " [\"code:16\"] = \"7\",", + " [\"code:17\"] = \"8\",", + " [\"code:18\"] = \"9\",", + " [\"code:19\"] = \"0\",", + " }", + " if mods:match(\"SHIFT\") and shifted_number_keys[key] then", + " local number_key = number_keys[key]", + " if number_key then", + " return { shifted_number_keys[key], number_key }", + " end", + " return { shifted_number_keys[key] }", + " end", + " if number_keys[key] then", + " return { number_keys[key] }", + " end", + " return { key }", + "end", + "", + "local function workspace_value(value)", + " value = trim(value)", + " return tonumber(value) or value", + "end", + "", + "local function direction(value)", + " local directions = {", + " l = \"left\",", + " r = \"right\",", + " u = \"up\",", + " d = \"down\",", + " left = \"left\",", + " right = \"right\",", + " up = \"up\",", + " down = \"down\",", + " }", + " return directions[trim(value)] or trim(value)", + "end", + "", + "local function dispatch(name, args)", + " local window_api = (dsp and dsp.window) or hl.window or {}", + " name = trim(name)", + " args = trim(args)", + " if name == \"exec\" then", + " return exec_cmd(args)", + " end", + " if name == \"killactive\" and window_api.close then", + " return window_api.close()", + " end", + " if name == \"fullscreen\" and window_api.fullscreen then", + " if args == \"1\" then", + " return window_api.fullscreen({ mode = \"maximized\" })", + " end", + " return window_api.fullscreen({ mode = \"fullscreen\" })", + " end", + " if name == \"movefocus\" and dsp and dsp.focus then", + " return function()", + " local ok, dispatcher = pcall(dsp.focus, { direction = direction(args) })", + " if ok and dispatcher then", + " hl.dispatch(dispatcher)", + " end", + " end", + " end", + " if name == \"cyclenext\" then", + " if args == \"prev\" or args == \"b\" then", + " return exec_cmd(\"$HOME/.config/hypr/scripts/LuaCycleWindow.sh previous\")", + " end", + " return exec_cmd(\"$HOME/.config/hypr/scripts/LuaCycleWindow.sh next\")", + " end", + " if name == \"swapwindow\" then", + " local swap_direction = trim(args)", + " if swap_direction == \"\" then", + " return nil", + " end", + " return exec_cmd(\"$HOME/.config/hypr/scripts/LuaSwapWindow.sh \" .. swap_direction)", + " end", + " if name == \"workspace\" and dsp and dsp.focus then", + " return function() hl.dispatch(dsp.focus({ workspace = workspace_value(args) })) end", + " end", + " if name == \"movetoworkspace\" and window_api.move then", + " return function() hl.dispatch(window_api.move({ workspace = workspace_value(args) })) end", + " end", + " if name == \"movetoworkspacesilent\" and window_api.move then", + " return function() hl.dispatch(window_api.move({ workspace = workspace_value(args), follow = false })) end", + " end", + " if name == \"togglefloating\" and window_api.float then", + " return function() hl.dispatch(window_api.float({ action = \"toggle\" })) end", + " end", + " if name == \"resizewindow\" and window_api.resize then", + " return window_api.resize()", + " end", + " if name == \"resizeactive\" and window_api.resize then", + " local x, y = args:match(\"^(%-?%d+)%s+(%-?%d+)$\")", + " if x and y then", + " return window_api.resize({ x = tonumber(x) or 0, y = tonumber(y) or 0, relative = true })", + " end", + " end", + " if name == \"movewindow\" and args == \"\" and window_api.drag then", + " return window_api.drag()", + " end", + " if args ~= \"\" then", + " return raw_dispatch_cmd(name .. \" \" .. args)", + " end", + " return raw_dispatch_cmd(name)", + "end", + "", + "local function bind(mods, key, fn, opts)", + " local seen = {}", + " for _, key_variant in ipairs(key_variants(key, mods)) do", + " local key_chord = chord(mods, key_variant)", + " if not seen[key_chord] then", + " seen[key_chord] = true", + " if opts then", + " hl.bind(key_chord, fn, opts)", + " else", + " hl.bind(key_chord, fn)", + " end", + " end", + " end", + "end", + "", + "local function unbind(mods, key)", + " if hl.unbind then", + " local seen = {}", + " for _, key_variant in ipairs(key_variants(key, mods)) do", + " local key_chord = chord(mods, key_variant)", + " if not seen[key_chord] then", + " seen[key_chord] = true", + " local ok = pcall(hl.unbind, mods, key_variant)", + " if not ok then", + " pcall(hl.unbind, key_chord)", + " end", + " end", + " end", + " end", + "end", + "", +] +if system_keybinds: + system_keybind_lines.append("-- Converted from configs/Keybinds.conf") + system_keybind_lines.extend(system_keybinds) + write_file(files_out["system_keybinds"], system_keybind_lines) +else: + if files_out["system_keybinds"].exists(): + print(f"[INFO] No active keybind entries found in {system_keybinds_path}; keeping existing {files_out['system_keybinds']}") + else: + system_keybind_lines.append("-- No active keybind entries were found in configs/Keybinds.conf.") + write_file(files_out["system_keybinds"], system_keybind_lines) + +for name, source in [ + ("system_settings", system_settings_path), + ("system_laptops", system_laptops_path), +]: + title = f"-- {name.replace('_', ' ').title()} (auto-generated)." + lines = [ + title, + "-- This file keeps migrated settings split from user overrides.", + "-- Add only Lua entries here.", + "-- Example:", + "-- hl.config({ general = { gaps_in = 4, gaps_out = 8 } })", + "", + ] + source_path = source if source.exists() else latest_legacy_file(source) + if source_path and source_path != source: + print(f"[INFO] {source.name} not found at {source}; using legacy source {source_path}") + + if name == "system_settings": + if source_path is None: + lines.append(f"-- No active entries were found in {source.name}.") + write_file(files_out[name], lines) + else: + scripts_dir = parse_scripts_dir(source_path) + if scripts_dir: + lines.append(f"local scriptsDir = {lua_string(scripts_dir)}") + lines.append("") + + sections = parse_hyprlang_sections(source_path) + gestures_section = sections.pop("gestures", None) + if gestures_section and "gesture" in gestures_section: + gestures_section.pop("gesture", None) + + ordered_sections = [ + "dwindle", + "master", + "scrolling", + "general", + "input", + "gestures", + "misc", + "binds", + "xwayland", + "render", + "cursor", + ] + for section in ordered_sections: + if section == "gestures": + data = gestures_section + elif section == "misc": + data = sections.get(section) or {} + if "force_default_wallpaper" not in data: + data["force_default_wallpaper"] = "false" + else: + data = sections.get(section) + if not data: + continue + lines.append("hl.config({") + lines.append(f" {section} = {{") + lines.extend(render_table(data, indent=4)) + lines.append(" },") + lines.append("})") + lines.append("") + + simple_gestures, complex_gestures = parse_gestures(source_path) + for spec in simple_gestures: + lines.extend([ + "hl.gesture({", + f" fingers = {spec['fingers']},", + f" direction = {lua_string(spec['direction'])},", + f" action = {lua_string(spec['action'])},", + "})", + "", + ]) + + if complex_gestures: + lines.append("-- Complex dispatcher gestures from SystemSettings.conf are pending explicit Lua API parity:") + for entry in complex_gestures: + lines.append(f"-- gesture = {entry}") + lines.append("") + + write_file(files_out[name], lines) + continue + + reference = source_examples(source_path) if source_path else [] + if reference: + lines.extend([ + f"-- Source reference from {source_path.name} (hyprlang):", + *reference, + ]) + else: + lines.append(f"-- No active entries were found in {source.name}.") + write_file(files_out[name], lines) + +env_lines = [ + "-- User ENV overrides (auto-generated).", + "-- Add values using: hl.env(\"KEY\", \"VALUE\")", + "-- Example:", + "-- hl.env(\"MOZ_ENABLE_WAYLAND\", \"1\")", + "", +] +if env_entries: + env_lines.append("-- Converted from ENVariables.conf") + for key, value in env_entries: + env_lines.append(f"hl.env({lua_string(key)}, {lua_string(value)})") + write_file(files_out["env"], env_lines) +else: + if files_out["env"].exists(): + print(f"[INFO] No active env entries found in {env_path}; keeping existing {files_out['env']}") + else: + env_lines.extend([ + "-- No active env entries were found in ENVariables.conf.", + "-- Uncomment and customize examples below:", + "-- hl.env(\"GDK_SCALE\", \"1\")", + "-- hl.env(\"QT_SCALE_FACTOR\", \"1\")", + ]) + write_file(files_out["env"], env_lines) + +startup_lines = [ + "-- User startup overrides (auto-generated).", + "-- Add commands with exec_once(\"your command\")", + "-- Example:", + "-- exec_once(\"$HOME/.config/hypr/UserScripts/MyStartup.sh\")", + "", + "local user_startup_helper = nil", + "do", + " local source = (debug.getinfo(1, \"S\") or {}).source or \"\"", + " local source_path = source:match(\"^@(.+)$\")", + " local source_dir = source_path and source_path:match(\"^(.*)/[^/]+$\") or nil", + " local home = os.getenv(\"HOME\") or \"\"", + " local candidate_paths = {", + " source_dir and (source_dir .. \"/../lua/user_startup_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/lua/user_startup_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/user_startup_helper.lua\") or nil,", + " }", + "", + " local tried_paths = {}", + " for _, helper_path in ipairs(candidate_paths) do", + " if helper_path then", + " table.insert(tried_paths, helper_path)", + " local f = io.open(helper_path, \"r\")", + " if f then", + " f:close()", + " local loaded_ok, loaded_helpers = pcall(dofile, helper_path)", + " if loaded_ok and type(loaded_helpers) == \"table\" and loaded_helpers.exec_once then", + " user_startup_helper = loaded_helpers", + " break", + " end", + " end", + " end", + " end", + "", + " if not user_startup_helper then", + " error(\"Failed to load user_startup_helper.lua from: \" .. table.concat(tried_paths, \", \"))", + " end", + "end", + "local exec_once = user_startup_helper.exec_once", + "", +] +if startup_entries: + startup_lines.append("-- Converted from Startup_Apps.conf") + startup_lines.append("local startup_commands = {") + for cmd in startup_entries: + startup_lines.append(f" {lua_string(cmd)},") + startup_lines.extend([ + "}", + "", + "local function run_startup_commands()", + " for _, cmd in ipairs(startup_commands) do", + " exec_once(cmd)", + " end", + "end", + "", + "if hl and hl.on then", + " hl.on(\"hyprland.start\", run_startup_commands)", + "else", + " run_startup_commands()", + "end", + ]) + write_file(files_out["startup"], startup_lines) +else: + if files_out["startup"].exists(): + print(f"[INFO] No active startup entries found in {startup_path}; keeping existing {files_out['startup']}") + else: + startup_lines.extend([ + "-- No active startup entries were found in Startup_Apps.conf.", + "-- exec_once(\"nm-applet --indicator\")", + ]) + write_file(files_out["startup"], startup_lines) + +window_lines = [ + "-- User window rule overrides (auto-generated).", + "-- Add your own rules with apply_window_rule({...})", + "-- Example:", + "-- apply_window_rule({", + "-- name = \"My Float Rule\",", + "-- match = { class = \"^pavucontrol$\" },", + "-- float = true,", + "-- center = true,", + "-- })", + "", + "local user_window_rules_helper = nil", + "do", + " local source = (debug.getinfo(1, \"S\") or {}).source or \"\"", + " local source_path = source:match(\"^@(.+)$\")", + " local source_dir = source_path and source_path:match(\"^(.*)/[^/]+$\") or nil", + " local home = os.getenv(\"HOME\") or \"\"", + " local candidate_paths = {", + " source_dir and (source_dir .. \"/../lua/user_window_rules_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/lua/user_window_rules_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/user_window_rules_helper.lua\") or nil,", + " }", + "", + " local tried_paths = {}", + " for _, helper_path in ipairs(candidate_paths) do", + " if helper_path then", + " table.insert(tried_paths, helper_path)", + " local f = io.open(helper_path, \"r\")", + " if f then", + " f:close()", + " local loaded_ok, loaded_helpers = pcall(dofile, helper_path)", + " if loaded_ok and type(loaded_helpers) == \"table\" and loaded_helpers.apply_window_rule then", + " user_window_rules_helper = loaded_helpers", + " break", + " end", + " end", + " end", + " end", + "", + " if not user_window_rules_helper then", + " error(\"Failed to load user_window_rules_helper.lua from: \" .. table.concat(tried_paths, \", \"))", + " end", + "end", + "local apply_window_rule = user_window_rules_helper.apply_window_rule", + "", +] +if window_rules: + window_lines.append("-- Converted from WindowRules.conf") + for rule_type, rule in window_rules: + window_lines.append(emit_rule(rule_type, rule)) + window_lines.append("") +else: + window_lines.append("-- No active window rules were found in WindowRules.conf.") +write_file(files_out["window_rules"], window_lines) + +layer_lines = [ + "-- User layer rule overrides (auto-generated).", + "-- Add your own rules with apply_layer_rule({...})", + "-- Example:", + "-- apply_layer_rule({", + "-- name = \"My Layer Rule\",", + "-- match = { namespace = \"notifications\" },", + "-- blur = true,", + "-- })", + "", + "local user_layer_rules_helper = nil", + "do", + " local source = (debug.getinfo(1, \"S\") or {}).source or \"\"", + " local source_path = source:match(\"^@(.+)$\")", + " local source_dir = source_path and source_path:match(\"^(.*)/[^/]+$\") or nil", + " local home = os.getenv(\"HOME\") or \"\"", + " local candidate_paths = {", + " source_dir and (source_dir .. \"/../lua/user_layer_rules_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/lua/user_layer_rules_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/user_layer_rules_helper.lua\") or nil,", + " }", + "", + " local tried_paths = {}", + " for _, helper_path in ipairs(candidate_paths) do", + " if helper_path then", + " table.insert(tried_paths, helper_path)", + " local f = io.open(helper_path, \"r\")", + " if f then", + " f:close()", + " local loaded_ok, loaded_helpers = pcall(dofile, helper_path)", + " if loaded_ok and type(loaded_helpers) == \"table\" and loaded_helpers.apply_layer_rule then", + " user_layer_rules_helper = loaded_helpers", + " break", + " end", + " end", + " end", + " end", + "", + " if not user_layer_rules_helper then", + " error(\"Failed to load user_layer_rules_helper.lua from: \" .. table.concat(tried_paths, \", \"))", + " end", + "end", + "local apply_layer_rule = user_layer_rules_helper.apply_layer_rule", + "", +] +if layer_rules: + layer_lines.append("-- Converted from LayerRules.conf") + for rule_type, rule in layer_rules: + layer_lines.append(emit_rule(rule_type, rule)) + layer_lines.append("") +else: + layer_lines.append("-- No active layer rules were found in LayerRules.conf.") +write_file(files_out["layer_rules"], layer_lines) + +keybind_lines = [ + "-- User keybind overrides (auto-generated).", + "-- Add keybinds with bind(\"MODS\", \"KEY\", fn, opts).", + "-- Example:", + "-- bind(\"SUPER\", \"Z\", exec_cmd(\"ghostty\"), { description = \"Launch ghostty\" })", + "-- Helper functions live in ~/.config/hypr/lua/user_keybinds_helper.lua so they can be updated separately.", + "local user_keybinds_helper = nil", + "do", + " local source = (debug.getinfo(1, \"S\") or {}).source or \"\"", + " local source_path = source:match(\"^@(.+)$\")", + " local source_dir = source_path and source_path:match(\"^(.*)/[^/]+$\") or nil", + " local home = os.getenv(\"HOME\") or \"\"", + " local candidate_paths = {", + " source_dir and (source_dir .. \"/../lua/user_keybinds_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/lua/user_keybinds_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/user_keybinds_helper.lua\") or nil,", + " }", + "", + " local tried_paths = {}", + " for _, helper_path in ipairs(candidate_paths) do", + " if helper_path then", + " table.insert(tried_paths, helper_path)", + " local f = io.open(helper_path, \"r\")", + " if f then", + " f:close()", + " local loaded_ok, loaded_helpers = pcall(dofile, helper_path)", + " if loaded_ok and type(loaded_helpers) == \"table\" and loaded_helpers.bind then", + " user_keybinds_helper = loaded_helpers", + " break", + " end", + " end", + " end", + " end", + "", + " if not user_keybinds_helper then", + " error(\"Failed to load user_keybinds_helper.lua from: \" .. table.concat(tried_paths, \", \"))", + " end", + "end", + "local exec_cmd = user_keybinds_helper.exec_cmd", + "local dispatch = user_keybinds_helper.dispatch", + "local bind = user_keybinds_helper.bind", + "local unbind = user_keybinds_helper.unbind", + "", +] +if keybinds: + keybind_lines.append("-- Converted from UserKeybinds.conf") + keybind_lines.extend(keybinds) + write_file(files_out["keybinds"], keybind_lines) +else: + if files_out["keybinds"].exists(): + print(f"[INFO] No active keybind entries found in {keybinds_path}; keeping existing {files_out['keybinds']}") + else: + keybind_lines.extend([ + "-- No active keybind entries were found in UserKeybinds.conf.", + "-- bind(\"SUPER\", \"Z\", exec_cmd(\"thunar\"), { description = \"Open file manager\" })", + ]) + write_file(files_out["keybinds"], keybind_lines) + +for name, source in [ + ("settings", settings_path), + ("decorations", decorations_path), + ("animations", animations_path), + ("laptops", laptops_path), +]: + title = f"-- User {name} overrides (auto-generated)." + lines = [ + title, + "-- This file is intentionally split from other user overrides.", + "-- Add only user-specific Lua overrides here.", + "-- Example:", + "-- hl.config({ general = { gaps_in = 4, gaps_out = 8 } })", + "", + ] + reference = source_examples(source) + if name == "decorations": + if reference or not files_out[name].exists(): + parsed_decorations = parse_hyprlang_sections(source) + decoration_lines = [ + title, + "-- This file is intentionally split from other user overrides.", + "-- Add only user-specific Lua overrides here.", + "-- Reads active border/shadow colors from wallust-hyprland.conf.", + "", + "local config_home = os.getenv(\"XDG_CONFIG_HOME\") or ((os.getenv(\"HOME\") or \"\") .. \"/.config\")", + "local wallust_colors_file = config_home .. \"/hypr/wallust/wallust-hyprland.conf\"", + "", + "local user_decorations_helper = nil", + "do", + " local source = (debug.getinfo(1, \"S\") or {}).source or \"\"", + " local source_path = source:match(\"^@(.+)$\")", + " local source_dir = source_path and source_path:match(\"^(.*)/[^/]+$\") or nil", + " local home = os.getenv(\"HOME\") or \"\"", + " local candidate_paths = {", + " source_dir and (source_dir .. \"/../lua/user_decorations_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/lua/user_decorations_helper.lua\") or nil,", + " home ~= \"\" and (home .. \"/.config/hypr/user_decorations_helper.lua\") or nil,", + " }", + "", + " local tried_paths = {}", + " for _, helper_path in ipairs(candidate_paths) do", + " if helper_path then", + " table.insert(tried_paths, helper_path)", + " local f = io.open(helper_path, \"r\")", + " if f then", + " f:close()", + " local loaded_ok, loaded_helpers = pcall(dofile, helper_path)", + " if loaded_ok and type(loaded_helpers) == \"table\" and loaded_helpers.load_wallust_colors then", + " user_decorations_helper = loaded_helpers", + " break", + " end", + " end", + " end", + " end", + "", + " if not user_decorations_helper then", + " error(\"Failed to load user_decorations_helper.lua from: \" .. table.concat(tried_paths, \", \"))", + " end", + "end", + "local load_wallust_colors = user_decorations_helper.load_wallust_colors", + "", + "local wallust = load_wallust_colors(wallust_colors_file)", + "local function wallust_color(name, fallback)", + " local value = wallust[name]", + " if value ~= nil then", + " return value", + " end", + " return fallback", + "end", + ] + if parsed_decorations: + for section_name in ("general", "decoration", "group"): + section = parsed_decorations.get(section_name) + if not section: + continue + decoration_lines.append("") + decoration_lines.append("hl.config({") + decoration_lines.append(f" {section_name} = {{") + decoration_lines.extend(render_table(section, indent=4)) + decoration_lines.append(" },") + decoration_lines.append("})") + else: + decoration_lines.extend([ + "", + "hl.config({", + " general = {", + " border_size = 2,", + " gaps_in = 2,", + " gaps_out = 4,", + " col = {", + " active_border = wallust_color(\"color12\", \"rgba(8db4ffff)\"),", + " inactive_border = wallust_color(\"color10\", \"rgba(5f6578ff)\"),", + " },", + " },", + "})", + "", + "hl.config({", + " decoration = {", + " rounding = 10,", + " active_opacity = 1.0,", + " inactive_opacity = 0.9,", + " fullscreen_opacity = 1.0,", + " dim_inactive = true,", + " dim_strength = 0.1,", + " dim_special = 0.8,", + " shadow = {", + " enabled = true,", + " range = 3,", + " render_power = 1,", + " color = wallust_color(\"color12\", \"rgba(8db4ffff)\"),", + " color_inactive = wallust_color(\"color10\", \"rgba(5f6578ff)\"),", + " },", + " blur = {", + " enabled = true,", + " size = 6,", + " passes = 3,", + " new_optimizations = true,", + " xray = true,", + " ignore_opacity = true,", + " special = true,", + " popups = true,", + " },", + " },", + "})", + "", + "hl.config({", + " group = {", + " col = {", + " border_active = wallust_color(\"color15\", \"rgba(ffffffff)\"),", + " },", + " groupbar = {", + " col = {", + " active = wallust_color(\"color0\", \"rgba(0f111aff)\"),", + " },", + " },", + " },", + "})", + ]) + if reference: + decoration_lines.extend([ + "", + f"-- Source reference from {source.name} (hyprlang):", + *reference, + ]) + write_file(files_out[name], decoration_lines) + else: + print(f"[INFO] No active entries found in {source}; keeping existing {files_out[name]}") + continue + if reference: + lines.extend([ + f"-- Source reference from {source.name} (hyprlang):", + *reference, + ]) + write_file(files_out[name], lines) + else: + if files_out[name].exists(): + print(f"[INFO] No active entries found in {source}; keeping existing {files_out[name]}") + else: + lines.append(f"-- No active entries were found in {source.name}.") + write_file(files_out[name], lines) +PY + +cat > "$USER_OVERRIDES_SHIM" <<'LUA' +-- ================================================== +-- KoolDots (2026) +-- Project URL: https://github.com/LinuxBeginnings +-- License: GNU GPLv3 +-- SPDX-License-Identifier: GPL-3.0-or-later +-- ================================================== +-- Auto-generated by scripts/migrate-hypr-to-lua.sh. +-- Loads split system/user Lua files from ~/.config/hypr/configs and ~/.config/hypr/UserConfigs. +local configHome = os.getenv("XDG_CONFIG_HOME") or ((os.getenv("HOME") or "") .. "/.config") +local hyprDir = configHome .. "/hypr" +local systemDir = hyprDir .. "/configs" +local userDir = configHome .. "/hypr/UserConfigs" + +local function load_optional(path) + local ok, err = pcall(dofile, path) + if ok then + return true + end + if err and tostring(err):find("No such file or directory", 1, true) ~= nil then + return false + end + print("[WARN] Unable to load user override file " .. path .. ": " .. tostring(err)) + return false +end + +local system_files = { + "system_env.lua", + "system_startup.lua", + "system_window_rules.lua", + "system_layer_rules.lua", + "system_keybinds.lua", + "system_settings.lua", + "system_laptops.lua", +} +for _, file in ipairs(system_files) do + local primary = systemDir .. "/" .. file + local legacy = userDir .. "/" .. file + if not load_optional(primary) then + load_optional(legacy) + end +end +local loaded_user_split = false + +local user_files = { + "user_env.lua", + "user_startup.lua", + "user_window_rules.lua", + "user_layer_rules.lua", + "user_keybinds.lua", + "user_settings.lua", + "user_decorations.lua", + "user_animations.lua", + "user_laptops.lua", +} +for _, file in ipairs(user_files) do + local path = userDir .. "/" .. file + if load_optional(path) then + loaded_user_split = true + end +end +if not loaded_user_split then + load_optional(userDir .. "/user_overrides.lua") -- backward compatibility with older single-file overrides +end +LUA + +move_conf_files_to_legacy() { + local source_dir="$1" + local legacy_dir="$2" + local label="$3" + local moved=0 + local file + + [ -d "$source_dir" ] || return 0 + mkdir -p "$legacy_dir" + + while IFS= read -r -d '' file; do + mv "$file" "$legacy_dir/" + moved=1 + done < <(find "$source_dir" -maxdepth 1 -type f -name '*.conf' -print0) + + if [ "$moved" -eq 1 ]; then + echo "[OK] Moved $label/*.conf -> $legacy_dir" + fi +} +print_conversion_coverage_summary() { + echo "[INFO] Migration coverage summary (Hyprland Lua mode):" + cat <<SUMMARY +[INFO] Converted .conf -> .lua: +[INFO] - $DEST_MONITORS_CONF -> $DEST_LUA_MONITORS +[INFO] - $DEST_WORKSPACES_CONF -> $DEST_LUA_WORKSPACES +[INFO] - $SYSTEM_ENV_VARS -> $CONFIGS_DIR/system_env.lua +[INFO] - $SYSTEM_STARTUP_APPS -> $CONFIGS_DIR/system_startup.lua +[INFO] - $SYSTEM_WINDOW_RULES -> $CONFIGS_DIR/system_window_rules.lua +[INFO] - $SYSTEM_LAYER_RULES -> $CONFIGS_DIR/system_layer_rules.lua +[INFO] - $SYSTEM_KEYBINDS -> $CONFIGS_DIR/system_keybinds.lua +[INFO] - $SYSTEM_SETTINGS -> $CONFIGS_DIR/system_settings.lua +[INFO] - $SYSTEM_LAPTOPS -> $CONFIGS_DIR/system_laptops.lua +[INFO] - $USER_ENV_VARS -> $USER_CONFIGS_DIR/user_env.lua +[INFO] - $USER_STARTUP_APPS -> $USER_CONFIGS_DIR/user_startup.lua +[INFO] - $USER_WINDOW_RULES -> $USER_CONFIGS_DIR/user_window_rules.lua +[INFO] - $USER_LAYER_RULES -> $USER_CONFIGS_DIR/user_layer_rules.lua +[INFO] - $USER_KEYBINDS -> $USER_CONFIGS_DIR/user_keybinds.lua +[INFO] - $USER_SETTINGS -> $USER_CONFIGS_DIR/user_settings.lua +[INFO] - $USER_DECORATIONS -> $USER_CONFIGS_DIR/user_decorations.lua +[INFO] - $USER_ANIMATIONS -> $USER_CONFIGS_DIR/user_animations.lua +[INFO] - $USER_LAPTOPS -> $USER_CONFIGS_DIR/user_laptops.lua +[INFO] - $USER_CONFIGS_DIR/01-UserDefaults.conf -> $USER_CONFIGS_DIR/user_defaults.lua +[INFO] Intentionally native/template .conf files: +[INFO] - $DEST_HYPR_DIR/hypridle.conf +[INFO] - $DEST_HYPR_DIR/hyprlock.conf, hyprlock-1080p.conf, hyprlock-2k.conf +[INFO] - $DEST_HYPR_DIR/hyprland.conf (fallback/non-Lua entrypoint) +[INFO] - $DEST_HYPR_DIR/Monitor_Profiles/*.conf and $DEST_HYPR_DIR/animations/*.conf (preset profiles) +[INFO] - $USER_CONFIGS_DIR/LaptopDisplay.conf and $USER_CONFIGS_DIR/WorkSpaceRules.conf (legacy/helper files) +SUMMARY +} + +move_conf_files_to_legacy "$USER_CONFIGS_DIR" "$USER_CONFIGS_LEGACY_DIR" "$USER_CONFIGS_DIR" +move_conf_files_to_legacy "$CONFIGS_DIR" "$CONFIGS_LEGACY_DIR" "$CONFIGS_DIR" +print_conversion_coverage_summary + +echo "[OK] Lua Hyprland config copied." +echo "[INFO] Restart Hyprland to test Lua config pickup." +echo "[INFO] To rollback: $(basename "$0") --revert" |
