From 1234452a6ecba30826ac6ca58f323bd1fc152a82 Mon Sep 17 00:00:00 2001 From: "Muhamed H. Asadi" Date: Mon, 7 Sep 2026 23:19:54 +0330 Subject: [PATCH] feat: add automated konsolerc geometry sanitization and git backup integration - Implement [MainWindow] section stripping in backup and restore scripts to prevent screen geometry/monitor layout noise across devices - Add robust hostname detection and customizable archive naming in backup.sh - Add optional interactive git commit and push prompt upon successful backup - Update restore.sh diff preview and extraction to respect sanitized konsolerc - Add AGENTS.md documentation detailing script architecture and conventions --- AGENTS.md | 31 ++++++++++++++++ backup.sh | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++--- restore.sh | 72 +++++++++++++++++++++++++++++------ 3 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dbd8a11 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +## Overview +Lightweight Bash utility scripts for KDE Konsole configuration backup, cross-device synchronization, and restoration. + +## Key Features & Commands + +- **Backup**: `./backup.sh` + - Automatically strips device-specific screen geometry / monitor layouts (`[MainWindow]` section in `konsolerc`) to ensure clean cross-device sync. + - Staged in `backups/` with default naming format: `konsole_backup__YYYYMMDD_HHMMSS.zip`. + - Prompts user to confirm or edit the backup archive filename before saving. + - Enforces a retention policy of max 10 backups (older ones deleted and removed via `git rm` if inside a git repository). + - Prompts to commit and push the new backup to git remote. + +- **Restore**: `./restore.sh [optional_zip_path]` + - Supports positional arguments, stdin piping (`echo "file.zip" | ./restore.sh`), and an interactive selection menu if no input is provided. + - Sanitizes `konsolerc` to avoid overwriting the target machine's window/screen geometry. + - Supports file-by-file diff preview (`[d]`) using `git diff --no-index` or `diff`. + - Reads interactive prompts safely from `/dev/tty` when stdin is redirected. + +## Target Paths (Relative to `$HOME`) +- `.config/konsolerc` (sanitized of `[MainWindow]` geometry) +- `.local/share/konsole` (profiles, color schemes, bookmarks, keytabs) +- `.local/share/kxmlgui5/konsole` (UI toolbars / actions) +- `.config/konsole.notifyrc` +- `.config/konsolesshconfig` + +## Implementation Details & Conventions +- **Shell**: Pure Bash 4+ with `set -euo pipefail`. +- **Safety**: Uses `mktemp -d` for temporary staging directories with `trap ... EXIT` cleanup. +- **Verification**: Run `bash -n backup.sh restore.sh` to check syntax. diff --git a/backup.sh b/backup.sh index 7a40439..e46058d 100755 --- a/backup.sh +++ b/backup.sh @@ -6,10 +6,33 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKUP_DIR="${SCRIPT_DIR}/backups" mkdir -p "${BACKUP_DIR}" -# Timestamp for archive name +# Pre-flight requirements check for core tools +for cmd in zip awk find mktemp; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error: Required tool '${cmd}' is not installed or not in PATH." >&2 + exit 1 + fi +done + +# Determine device hostname using robust fallbacks (no inetutils required) +HOSTNAME_STR="" +if command -v hostname >/dev/null 2>&1; then + HOSTNAME_STR="$(hostname -s 2>/dev/null || hostname 2>/dev/null || true)" +fi +if [ -z "${HOSTNAME_STR}" ]; then + HOSTNAME_STR="${HOST:-${HOSTNAME:-}}" +fi +if [ -z "${HOSTNAME_STR}" ] && [ -r /proc/sys/kernel/hostname ]; then + HOSTNAME_STR="$(cat /proc/sys/kernel/hostname 2>/dev/null || true)" +fi +if [ -z "${HOSTNAME_STR}" ] && [ -r /etc/hostname ]; then + HOSTNAME_STR="$(cat /etc/hostname 2>/dev/null || true)" +fi +HOSTNAME_STR="$(echo "${HOSTNAME_STR:-}" | xargs)" +[ -z "${HOSTNAME_STR}" ] && HOSTNAME_STR="host" + TIMESTAMP="$(date +"%Y%m%d_%H%M%S")" -ZIP_FILENAME="konsole_backup_${TIMESTAMP}.zip" -ZIP_FILEPATH="${BACKUP_DIR}/${ZIP_FILENAME}" +DEFAULT_FILENAME="konsole_backup_${HOSTNAME_STR}_${TIMESTAMP}.zip" # Temporary staging directory TEMP_DIR="$(mktemp -d)" @@ -34,8 +57,18 @@ for rel_path in "${CONFIG_PATHS[@]}"; do if [ -e "${src_path}" ]; then mkdir -p "$(dirname "${dest_path}")" - cp -a "${src_path}" "${dest_path}" - echo " [+] Staged: ~/${rel_path}" + if [ "${rel_path}" = ".config/konsolerc" ]; then + # Filter out [MainWindow] section to avoid machine-specific geometry/monitor noise + awk ' + /^\[MainWindow\]/ { skip=1; next } + /^\[.*\]/ { skip=0 } + !skip { print } + ' "${src_path}" > "${dest_path}" + echo " [+] Staged & Sanitized: ~/${rel_path}" + else + cp -a "${src_path}" "${dest_path}" + echo " [+] Staged: ~/${rel_path}" + fi COPIED_COUNT=$((COPIED_COUNT + 1)) else echo " [-] Skipped (not found): ~/${rel_path}" @@ -47,6 +80,43 @@ if [ "${COPIED_COUNT}" -eq 0 ]; then exit 1 fi +# Determine appropriate interactive input source +get_input() { + local prompt_msg="$1" + local var_name="$2" + local default_val="${3:-}" + + if [ -t 0 ]; then + read -r -p "${prompt_msg}" "$var_name" || true + elif [ -c /dev/tty ] && [ -r /dev/tty ] && { : < /dev/tty; } 2>/dev/null; then + read -r -p "${prompt_msg}" "$var_name" < /dev/tty || true + else + read -r -p "${prompt_msg}" "$var_name" || true + fi + + if [ -z "${!var_name:-}" ] && [ -n "${default_val}" ]; then + eval "${var_name}=\"${default_val}\"" + fi +} + +echo "" +# Prompt user for backup filename (like ssh-keygen) +CHOSEN_NAME="" +get_input "Enter backup filename [default: ${DEFAULT_FILENAME}]: " CHOSEN_NAME "${DEFAULT_FILENAME}" +CHOSEN_NAME="$(echo "${CHOSEN_NAME:-}" | xargs)" + +if [ -z "${CHOSEN_NAME}" ]; then + ZIP_FILENAME="${DEFAULT_FILENAME}" +else + if [[ "${CHOSEN_NAME}" != *.zip ]]; then + ZIP_FILENAME="${CHOSEN_NAME}.zip" + else + ZIP_FILENAME="${CHOSEN_NAME}" + fi +fi + +ZIP_FILEPATH="${BACKUP_DIR}/${ZIP_FILENAME}" + # Create zip file from staging area echo "Creating backup archive: ${ZIP_FILENAME}..." ( @@ -67,8 +137,33 @@ if [ "${TOTAL_BACKUPS}" -gt "${MAX_BACKUPS}" ]; then for (( i=MAX_BACKUPS; i/dev/null; then + git rm -f "${file_to_remove}" 2>/dev/null || rm -f "${file_to_remove}" + else + rm -f "${file_to_remove}" + fi done fi echo "=== Backup Complete (${TOTAL_BACKUPS} backup(s) currently stored) ===" +echo "" + +# Optional Git Commit & Push +if git rev-parse --is-inside-work-tree &>/dev/null; then + git_choice="" + get_input "Do you want to commit and push this backup to git? [y/N]: " git_choice "n" + case "${git_choice,,}" in + y|yes) + echo "Staging backup file in git..." + git add "${ZIP_FILEPATH}" + COMMIT_MSG="chore(konsole): backup config from ${HOSTNAME_STR} at ${TIMESTAMP}" + echo "Committing: \"${COMMIT_MSG}\"..." + git commit -m "${COMMIT_MSG}" || true + echo "Pushing to remote repository..." + git push || echo "Warning: git push failed. Please check your git remote configuration." + ;; + *) + echo "Skipping git push." + ;; + esac +fi diff --git a/restore.sh b/restore.sh index e4b45c1..1be6e43 100755 --- a/restore.sh +++ b/restore.sh @@ -4,6 +4,20 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKUP_DIR="${SCRIPT_DIR}/backups" +# Function to sanitize a konsolerc file by stripping the [MainWindow] section +sanitize_konsolerc() { + local target_file="$1" + if [ -f "${target_file}" ]; then + local cleaned_file="${target_file}.clean" + awk ' + /^\[MainWindow\]/ { skip=1; next } + /^\[.*\]/ { skip=0 } + !skip { print } + ' "${target_file}" > "${cleaned_file}" + mv "${cleaned_file}" "${target_file}" + fi +} + show_diff() { local source_dir="$1" echo "" @@ -22,13 +36,36 @@ show_diff() { changes_found=1 echo -e "\n\033[1;32m[NEW FILE]\033[0m ~/${rel_path}" else - if ! cmp -s "${item}" "${dest_item}"; then - changes_found=1 - echo -e "\n\033[1;33m[MODIFIED]\033[0m ~/${rel_path}" - if command -v git &>/dev/null; then - git diff --no-index --color "${dest_item}" "${item}" || true - else - diff -u --color=always "${dest_item}" "${item}" || true + if [ "${rel_path}" = ".config/konsolerc" ]; then + # Compare against sanitized version of local konsolerc so [MainWindow] geometry noise is ignored + local temp_local_clean + temp_local_clean="$(mktemp)" + sanitize_konsolerc "${dest_item}" 2>/dev/null || true + awk ' + /^\[MainWindow\]/ { skip=1; next } + /^\[.*\]/ { skip=0 } + !skip { print } + ' "${dest_item}" > "${temp_local_clean}" + + if ! cmp -s "${item}" "${temp_local_clean}"; then + changes_found=1 + echo -e "\n\033[1;33m[MODIFIED]\033[0m ~/${rel_path}" + if command -v git &>/dev/null; then + git diff --no-index --color "${temp_local_clean}" "${item}" || true + else + diff -u --color=always "${temp_local_clean}" "${item}" || true + fi + fi + rm -f "${temp_local_clean}" + else + if ! cmp -s "${item}" "${dest_item}"; then + changes_found=1 + echo -e "\n\033[1;33m[MODIFIED]\033[0m ~/${rel_path}" + if command -v git &>/dev/null; then + git diff --no-index --color "${dest_item}" "${item}" || true + else + diff -u --color=always "${dest_item}" "${item}" || true + fi fi fi fi @@ -71,7 +108,13 @@ if [ -z "${ARCHIVE_PATH}" ]; then done echo " [q] Quit" - read -r -p "Select a backup to restore [1-${#AVAILABLE_BACKUPS[@]}]: " choice < /dev/tty + if [ -t 0 ]; then + read -r -p "Select a backup to restore [1-${#AVAILABLE_BACKUPS[@]}]: " choice || true + elif [ -c /dev/tty ] && [ -r /dev/tty ] && { : < /dev/tty; } 2>/dev/null; then + read -r -p "Select a backup to restore [1-${#AVAILABLE_BACKUPS[@]}]: " choice < /dev/tty || true + else + read -r -p "Select a backup to restore [1-${#AVAILABLE_BACKUPS[@]}]: " choice || true + fi if [[ "${choice}" =~ ^[0-9]+$ ]] && [ "${choice}" -ge 1 ] && [ "${choice}" -le "${#AVAILABLE_BACKUPS[@]}" ]; then ARCHIVE_PATH="${AVAILABLE_BACKUPS[$((choice - 1))]}" else @@ -102,6 +145,11 @@ trap 'rm -rf "${TEMP_DIR}"' EXIT unzip -q -o "${ARCHIVE_PATH}" -d "${TEMP_DIR}" +# Extra safety: Sanitize extracted konsolerc in case restoring an older archive +if [ -f "${TEMP_DIR}/.config/konsolerc" ]; then + sanitize_konsolerc "${TEMP_DIR}/.config/konsolerc" +fi + # Interactive confirmation loop (allows viewing diffs) while true; do echo "" @@ -113,13 +161,13 @@ while true; do echo "" if [ -t 0 ]; then - INPUT_SRC="/dev/stdin" + read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action || true + elif [ -c /dev/tty ] && [ -r /dev/tty ] && { : < /dev/tty; } 2>/dev/null; then + read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action < /dev/tty || true else - INPUT_SRC="/dev/tty" + read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action || true fi - read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action < "${INPUT_SRC}" - case "${action,,}" in r|y|yes) echo "Restoring configuration files..."