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
This commit is contained in:
@@ -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_<hostname>_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.
|
||||||
@@ -6,10 +6,33 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
BACKUP_DIR="${SCRIPT_DIR}/backups"
|
BACKUP_DIR="${SCRIPT_DIR}/backups"
|
||||||
mkdir -p "${BACKUP_DIR}"
|
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")"
|
TIMESTAMP="$(date +"%Y%m%d_%H%M%S")"
|
||||||
ZIP_FILENAME="konsole_backup_${TIMESTAMP}.zip"
|
DEFAULT_FILENAME="konsole_backup_${HOSTNAME_STR}_${TIMESTAMP}.zip"
|
||||||
ZIP_FILEPATH="${BACKUP_DIR}/${ZIP_FILENAME}"
|
|
||||||
|
|
||||||
# Temporary staging directory
|
# Temporary staging directory
|
||||||
TEMP_DIR="$(mktemp -d)"
|
TEMP_DIR="$(mktemp -d)"
|
||||||
@@ -34,8 +57,18 @@ for rel_path in "${CONFIG_PATHS[@]}"; do
|
|||||||
|
|
||||||
if [ -e "${src_path}" ]; then
|
if [ -e "${src_path}" ]; then
|
||||||
mkdir -p "$(dirname "${dest_path}")"
|
mkdir -p "$(dirname "${dest_path}")"
|
||||||
cp -a "${src_path}" "${dest_path}"
|
if [ "${rel_path}" = ".config/konsolerc" ]; then
|
||||||
echo " [+] Staged: ~/${rel_path}"
|
# 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))
|
COPIED_COUNT=$((COPIED_COUNT + 1))
|
||||||
else
|
else
|
||||||
echo " [-] Skipped (not found): ~/${rel_path}"
|
echo " [-] Skipped (not found): ~/${rel_path}"
|
||||||
@@ -47,6 +80,43 @@ if [ "${COPIED_COUNT}" -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# Create zip file from staging area
|
||||||
echo "Creating backup archive: ${ZIP_FILENAME}..."
|
echo "Creating backup archive: ${ZIP_FILENAME}..."
|
||||||
(
|
(
|
||||||
@@ -67,8 +137,33 @@ if [ "${TOTAL_BACKUPS}" -gt "${MAX_BACKUPS}" ]; then
|
|||||||
for (( i=MAX_BACKUPS; i<TOTAL_BACKUPS; i++ )); do
|
for (( i=MAX_BACKUPS; i<TOTAL_BACKUPS; i++ )); do
|
||||||
file_to_remove="${BACKUP_FILES[$i]}"
|
file_to_remove="${BACKUP_FILES[$i]}"
|
||||||
echo " Deleting old backup: $(basename "${file_to_remove}")"
|
echo " Deleting old backup: $(basename "${file_to_remove}")"
|
||||||
rm -f "${file_to_remove}"
|
if git rev-parse --is-inside-work-tree &>/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
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== Backup Complete (${TOTAL_BACKUPS} backup(s) currently stored) ==="
|
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
|
||||||
|
|||||||
+60
-12
@@ -4,6 +4,20 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
BACKUP_DIR="${SCRIPT_DIR}/backups"
|
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() {
|
show_diff() {
|
||||||
local source_dir="$1"
|
local source_dir="$1"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -22,13 +36,36 @@ show_diff() {
|
|||||||
changes_found=1
|
changes_found=1
|
||||||
echo -e "\n\033[1;32m[NEW FILE]\033[0m ~/${rel_path}"
|
echo -e "\n\033[1;32m[NEW FILE]\033[0m ~/${rel_path}"
|
||||||
else
|
else
|
||||||
if ! cmp -s "${item}" "${dest_item}"; then
|
if [ "${rel_path}" = ".config/konsolerc" ]; then
|
||||||
changes_found=1
|
# Compare against sanitized version of local konsolerc so [MainWindow] geometry noise is ignored
|
||||||
echo -e "\n\033[1;33m[MODIFIED]\033[0m ~/${rel_path}"
|
local temp_local_clean
|
||||||
if command -v git &>/dev/null; then
|
temp_local_clean="$(mktemp)"
|
||||||
git diff --no-index --color "${dest_item}" "${item}" || true
|
sanitize_konsolerc "${dest_item}" 2>/dev/null || true
|
||||||
else
|
awk '
|
||||||
diff -u --color=always "${dest_item}" "${item}" || true
|
/^\[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
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -71,7 +108,13 @@ if [ -z "${ARCHIVE_PATH}" ]; then
|
|||||||
done
|
done
|
||||||
echo " [q] Quit"
|
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
|
if [[ "${choice}" =~ ^[0-9]+$ ]] && [ "${choice}" -ge 1 ] && [ "${choice}" -le "${#AVAILABLE_BACKUPS[@]}" ]; then
|
||||||
ARCHIVE_PATH="${AVAILABLE_BACKUPS[$((choice - 1))]}"
|
ARCHIVE_PATH="${AVAILABLE_BACKUPS[$((choice - 1))]}"
|
||||||
else
|
else
|
||||||
@@ -102,6 +145,11 @@ trap 'rm -rf "${TEMP_DIR}"' EXIT
|
|||||||
|
|
||||||
unzip -q -o "${ARCHIVE_PATH}" -d "${TEMP_DIR}"
|
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)
|
# Interactive confirmation loop (allows viewing diffs)
|
||||||
while true; do
|
while true; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -113,13 +161,13 @@ while true; do
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ -t 0 ]; then
|
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
|
else
|
||||||
INPUT_SRC="/dev/tty"
|
read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
read -r -p "Choose action: [r] Restore / [d] View Diff / [c] Cancel: " action < "${INPUT_SRC}"
|
|
||||||
|
|
||||||
case "${action,,}" in
|
case "${action,,}" in
|
||||||
r|y|yes)
|
r|y|yes)
|
||||||
echo "Restoring configuration files..."
|
echo "Restoring configuration files..."
|
||||||
|
|||||||
Reference in New Issue
Block a user