Last active
September 11, 2025 08:42
-
-
Save dzogrim/59481f6ef0296dcba0f59a0bfc53c1d7 to your computer and use it in GitHub Desktop.
Provides an interactive system maintenance menu for macOS using 'dialog'
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# SPDX-License-Identifier: MIT | |
# SPDX-FileCopyrightText: 2015-2025 dzogrim | |
# SPDX-FileContributor: dzogrim <[email protected]> | |
# ----------------------------------------------------------------------------- | |
# refresh_system.sh — macOS Maintenance Toolkit | |
# | |
# A modular Bash script to run daily/weekly maintenance on macOS systems. | |
# Uses either dialog or gum as an interactive frontend, with fallback. | |
# Tasks include: | |
# - dotfiles and preferences sync (mackup) | |
# - updates for macOS, App Store, Homebrew, MacPorts, and Nix | |
# - Python version pinning | |
# - validation checks (SSL certs, Spotlight, arch analysis) | |
# | |
# Author : dzogrim (Sébastien L.) | |
# Maintainer : dzogrim (Sébastien L.) — MIT License | |
# Updated : 2025-09-11 | |
# Version : 2.9.0 | |
# System requirements: | |
# - macOS with dialog, jq, sudo, softwareupdate, brew, port, git, mackup, mas, mdutil & GNU bash 5 | |
# Optional: | |
# - MacUpdater 3.4 (proprietary) | |
# ----------------------------------------------------------------------------- | |
# Required subscripts in PATH: | |
# - bash_rc_reseync_v2.sh: https://gist.github.com/dzogrim/7b7aa378a6ad2fd19b83466f99e9d0a2 | |
# - git-refresh.sh: https://gist.github.com/dzogrim/1200bf4171b69ea9ded23e53b31746cb | |
# - brew_conf_export.sh: https://gist.github.com/dzogrim/834ca2bffde8055e013fe97501aaa0f1 | |
# - brew-update.sh: https://gist.github.com/dzogrim/00e3d5dd9e2b14680204fe4d5d58f8d3 | |
# - compare_brew_home_office.sh: (something like `comm -23 Brewfile_env_1 Brewfile_env_2 ...`) | |
# - compare_ports_home_office.sh: (something like `comm -23 backup_env_1 backup_env_2 ...`) | |
# - ports-infoBackup.sh: (something like `port -qv installed | sort > ...`) | |
# - ports-update.sh: (some workflow with `port -fc upgrade outdated`) | |
# - check_env_shell.sh: (some check inside ~/.bashrc) | |
# - reset_spotlight_position.sh: https://gist.github.com/dzogrim/21b57261ac80b161e8c95d164f2524c3 | |
# - maintenance-nix-macos.sh: https://gist.github.com/dzogrim/d106380d7a281f50d4f860635056af81 | |
# - nix-manager-adm: https://gist.github.com/dzogrim/72eeac8a844c6ee9bca0e4451994ce65 | |
# - set-python-123.sh: https://gist.github.com/dzogrim/215446f4dc0dd6bf945915f34bb84ff1 | |
# - remove-MAU2.sh: https://gist.github.com/dzogrim/92cd4143ee685cd48e0ede5614b8bb28 | |
# - certs_validity.sh: https://gist.github.com/dzogrim/c3a841e72fac41e8259483fea8aa92fd | |
# ----------------------------------------------------------------------------- | |
# shellcheck disable=SC2059 | |
# ___ _ _ _ | |
# / __| ___| |_| |_(_)_ _ __ _ ___ | |
# \__ \/ -_) _| _| | ' \/ _` (_-< | |
# |___/\___|\__|\__|_|_||_\__, /__/ | |
# |___/ | |
# Env. set based on username | |
readonly ENVHOME=user.perso | |
readonly ENVOFFICE=user-pro | |
# Python version | |
readonly PYVERS="313" # Ports compatible | |
readonly PYVERS_H="3.13.5" # Human friendly | |
# This version | |
VERSION="$(grep -E '^# Version\s+: ' "$0" | head -n1 | sed -E 's/^# Version\s+: ([^ ]+).*$/\1/')" | |
readonly VERSION | |
# Reset terminal and exit cleanly if interrupted (CTRL+C or termination signal). | |
trap 'stty sane; reset; echo "Aborted. Terminal reset."; exit 1' INT TERM | |
# Text color variables | |
txtbld=$(tput bold) # bold | |
bldred=${txtbld}$(tput setaf 1) # red | |
bldblu=${txtbld}$(tput setaf 4) # blue | |
bldbrw=${txtbld}$(tput setaf 3) # brown | |
bldgrn=${txtbld}$(tput setaf 2) # green | |
bldcya=${txtbld}$(tput setaf 6) # cyan | |
txtrst=$(tput sgr0) # reset | |
# Ensure the script is running on macOS only. | |
if [[ "$(uname)" != "Darwin" ]]; then | |
printf "\n${bldred}Error:${txtrst} This program is intended for macOS only. Exiting.\n" >&2 | |
exit 1 | |
fi | |
# Ensure LOGNAME is available | |
if [ -z "$LOGNAME" ]; then | |
LOGNAME=$(whoami) | |
printf "\n${bldbrw}Warning:${txtrst} LOGNAME was not set. Using 'whoami': %s\n" "$LOGNAME" >&2 | |
fi | |
# ___ _ _ | |
# | __| _ _ _ __| |_(_)___ _ _ ___ | |
# | _| || | ' \/ _| _| / _ \ ' \(_-< | |
# |_| \_,_|_||_\__|\__|_\___/_||_/__/ | |
# | |
# Verify that all required tools or scripts are available and executable in the system PATH. | |
checkTools() | |
{ | |
local misstools="" | |
# Check presence of necessary tools | |
for entry in "${@}" | |
do | |
if [[ ! -x "$(command -v "${entry}" 2>/dev/null)" ]]; | |
then | |
misstools="${misstools} ${entry}" | |
fi | |
done | |
if [ -z "${misstools}" ]; then | |
return | |
else | |
fatal "$(basename "${0}") requires the following missing tools: ${misstools}" | |
fi | |
} | |
# Simple usage display | |
usage() { | |
echo "" | |
echo "Usage:" | |
echo " $0 # Launch interactive menu (version $VERSION)" | |
echo " $0 --gum # Force gum UI only if installed" | |
echo " $0 --dialog # Force dialog UI even if gum is installed" | |
echo " $0 --debug-choices # Make sure cross-options are synced" | |
echo " $0 --list # Print available actions and their numeric IDs (for --run)" | |
echo " $0 --run <TAG> # Run a specific numbered action directly (non-interactive)" | |
echo "" | |
echo "Examples:" | |
echo " $0 --run 2 # Run 'Compare ~/.bashrc.d with synced version'" | |
echo " $0 --run 16 # Run 'Upgrade Nix environment'" | |
echo "" | |
exit 0 | |
} | |
# Set MacUpdater app path and options based on the current user environment. | |
envMacUpdaterApp() | |
{ | |
MACUPDTAPPOPTS="--hide-uptodate-apps --hide-unsupported-apps --quiet" | |
if [[ "${LOGNAME}" == "${ENVHOME}" ]]; then | |
MACUPDTAPP="/Applications/Unified Tools/System/MacUpdater.app/Contents/Resources/macupdater_client" | |
elif [[ "${LOGNAME}" == "${ENVOFFICE}" ]]; then | |
MACUPDTAPP="$HOME/Applications/MacUpdater.app/Contents/Resources/macupdater_client" | |
else | |
printf "\n${bldbrw}Warning:${txtrst} MacUpdater not found (or not set). Some features may not work properly.\n\n" | |
MACUPDTAPP="" | |
fi | |
} | |
# Check if MacUpdater is set before trying to start it. | |
checkMacUpdaterApp() { | |
if [[ ! -x "${MACUPDTAPP}" ]]; then | |
printf "\n${bldred}Error:${txtrst} MacUpdater app not found or not executable. Please verify its path.\n" | |
return 1 | |
fi | |
return 0 | |
} | |
# Cross-check OPTION numbers and CHOICE_LABEL mapping coherence (dev/debug use only) | |
debug_validate_choice_map() { | |
local errors=0 | |
for ((i = 0; i < ${#OPTIONS[@]}; i+=2)); do | |
local id="${OPTIONS[i]}" | |
[[ "$id" =~ ^[0-9]+$ ]] || continue | |
if [[ -z "${CHOICE_LABEL[$id]}" ]]; then | |
printf "\n${bldred}Error:${txtrst} OPTION [%s] has no corresponding CHOICE_LABEL.\n" "$id" >&2 | |
errors=$((errors + 1)) | |
fi | |
done | |
if (( errors > 0 )); then | |
fatal "Mismatch detected between OPTIONS and CHOICE_LABEL. Exiting." | |
fi | |
} | |
# Perform initial environment checks and set MacUpdater app configuration. | |
# Note: `gum` is not necessarily required. | |
initChecks() { | |
checkTools dialog mackup git port brew mas mdutil jq | |
envMacUpdaterApp | |
} | |
# Display a warning when root privileges are required. | |
warningRootDisplay() { | |
printf "\n${bldbrw}You need to gain root privileges to perform this action!${txtrst}\n" | |
} | |
# Display a warning if the current environment is unsupported. | |
warningBadEnv() { | |
printf "\n${bldbrw}Sorry!${txtrst} Nothing to do on this unknown env.\n" | |
} | |
# Display a fatal error message and exit the script. | |
fatal() { | |
stty sane 2>/dev/null | |
printf "\n${bldred}[FATAL]:${txtrst} %s\n" "$1" | |
exit 1 | |
} | |
# Display a confirmation message when the action is completed. | |
infoFinished() { | |
printf "\n${bldgrn}Done.${txtrst}\n" | |
} | |
# Nix maintenance snippet | |
nix_upgrade_func() { | |
checkTools maintenance-nix-macos.sh | |
if [[ "$NON_INTERACTIVE" != "1" ]]; then | |
read -r -t 30 -p "Deep clean? [y/N] " choice | |
case "$choice" in | |
[Yy]*) printf "\n" ; maintenance-nix-macos.sh --optimize --clean ;; | |
*) printf "\n" ; maintenance-nix-macos.sh ;; | |
esac | |
else | |
maintenance-nix-macos.sh | |
fi | |
} | |
# Retrieve the local Dropbox folder path (for pip backup etc.) | |
getDropboxPath() { | |
local json_path="$HOME/Library/Application Support/Dropbox/info.json" | |
local fallback="$HOME/Library/CloudStorage/Dropbox" | |
local dropbox_path="" | |
if [[ -f "$json_path" ]]; then | |
if command -v jq >/dev/null 2>&1; then | |
dropbox_path=$(jq -r '."personal"."path"' "$json_path" 2>/dev/null) | |
fi | |
if [[ -z "$dropbox_path" ]]; then | |
dropbox_path=$(grep -E '^\s*"path"\s*:' "$json_path" | \ | |
sed -E 's/.*"path": "([^"]+)".*/\1/' | head -n1) | |
fi | |
fi | |
if [[ -z "$dropbox_path" || ! -d "$dropbox_path" ]]; then | |
dropbox_path="$fallback" | |
fi | |
echo "$dropbox_path" | |
} | |
# Transform OPTIONS array into "key - label" format for gum choose | |
sanitize_gum_choices() { | |
for ((i=0; i<${#OPTIONS[@]}; i+=2)); do | |
key="${OPTIONS[i]}" | |
label="${OPTIONS[i+1]}" | |
if [[ "$key" =~ ^[0-9]+$ ]]; then | |
echo "$key - $label" | |
else | |
# Ignoring unused lines for gum ... | |
continue | |
fi | |
done | |
} | |
# Display the selected menu option dynamically based on OPTIONS array content. | |
# Highlights the option text in cyan to improve visibility. | |
printSelectedOption() { | |
local choice_num=$1 | |
for ((i = 0; i < ${#OPTIONS[@]}; i += 2)); do | |
if [[ "${OPTIONS[i]}" =~ ^[0-9]+$ && "${OPTIONS[i]}" -eq "$choice_num" ]]; then | |
printf "\n${bldblu}Selected Option${txtrst}: ${bldcya}%s${txtrst}\n" "${OPTIONS[i+1]}" | |
return | |
fi | |
done | |
printf "\n${bldred}Error:${txtrst} Selected option not found in OPTIONS array.\n" | |
} | |
# List MacPorts binaries that are x86_64 (not arm64), excluding noarch ports | |
report_x86_ports_func() { | |
# shellcheck disable=SC2034 | |
port -qv installed | while read -r portname version variant status; do | |
if port -v info "$portname" | grep -q 'Supported architectures: noarch'; then | |
continue | |
fi | |
local bin_path="/opt/local/bin/${portname}" | |
[[ ! -x "$bin_path" ]] && bin_path="/opt/local/sbin/${portname}" | |
[[ ! -x "$bin_path" ]] && continue | |
local archs=$(lipo -archs "$bin_path" 2>/dev/null || echo "unknown") | |
if [[ "$archs" != *arm64* ]]; then | |
echo "$portname" | |
fi | |
done | sort -u | |
} | |
# List Homebrew formulae containing x86_64 binaries (Intel-only) | |
report_x86_brews_func() { | |
local FINDBIN="/usr/bin/find" | |
brew list --formula | while read -r pkg; do | |
local bin_dir="$(brew --prefix "$pkg")/bin" | |
[[ -d "$bin_dir" ]] || continue | |
"$FINDBIN" "$bin_dir" -type f -perm +111 | while read -r bin; do | |
[[ ! -s "$bin" ]] && continue | |
local archs=$(lipo -archs "$bin" 2>/dev/null || echo "unknown") | |
if [[ "$archs" == "x86_64" ]]; then | |
echo "$pkg: $(basename "$bin") → $archs" | |
fi | |
done | |
done | sort -u | |
} | |
# Trigger a MacUpdater scan silently | |
macup_scan_func() { | |
checkMacUpdaterApp || return 1 | |
"${MACUPDTAPP}" scan --quiet | |
} | |
# List available app updates via MacUpdater, sorted, excluding scan metadata | |
macup_list_func() { | |
checkMacUpdaterApp || return 1 | |
# shellcheck disable=SC2086 | |
"${MACUPDTAPP}" list ${MACUPDTAPPOPTS} | grep -v -e "Last Scan Date" | sort --ignore-case | |
} | |
# Checks Desktop for something else than macOS aliases | |
userDesktopNotAliases_func() { | |
for i in ~/Desktop/*; do | |
out=$(file --brief "$i") | |
if [[ "$out" != "MacOS Alias file" ]]; then | |
printf "\033[1;36m%-30s\033[0m → \033[1;33m%s\033[0m\n" "$(basename "$i")" "$out" | |
fi | |
done | |
} | |
# Backup pip snippet | |
pip_backup_func() { | |
DROPBOX_PATH=$(getDropboxPath) | |
ENV_SUFFIX=$([[ "$LOGNAME" == "$ENVOFFICE" ]] && echo "office" || echo "home") | |
PATH_PREFIX="${DROPBOX_PATH}/Private/_SyncThat/confInfos" | |
PIP_OUTPUT="${PATH_PREFIX}/pipCompleteList_${ENV_SUFFIX}.txt" | |
if ! command -v pip >/dev/null 2>&1; then | |
fatal "pip is not installed or not in \$PATH." | |
fi | |
echo "Backing up current pip package list..." | |
mkdir -p "$PATH_PREFIX" | |
pip freeze > "$PIP_OUTPUT" | |
printf "✔︎ List saved to: ${bldgrn}%s${txtrst}\n" "$PIP_OUTPUT" | |
} | |
# Rebuild macOS Spotlight index (requires sudo) | |
rebuild_spotlight_func() { | |
warningRootDisplay | |
sudo mdutil -E / | |
} | |
# Run git-refresh.sh with parameters adapted to current environment | |
git_fetch_func() { | |
checkTools git-refresh.sh | |
if [[ "${LOGNAME}" == "${ENVOFFICE}" ]]; then | |
git-refresh.sh --skip --verbose | |
elif [[ "${LOGNAME}" == "${ENVHOME}" ]]; then | |
git-refresh.sh --discover --skip --verbose | |
else | |
echo "Unknown environment: $LOGNAME" | |
return 1 | |
fi | |
} | |
# Backup dotfiles using mackup and uninstall existing preferences | |
dotfiles_func() { | |
if [[ "$TERM_PROGRAM" == "iTerm.app" ]] || pgrep -q iTerm2; then | |
echo "⚠️ iTerm2 detected. Please quit it before running dotfiles backup." | |
return 2 | |
fi | |
mackup backup -f && mackup uninstall --force | |
} | |
# Executes a given command with its arguments but does NOT abort the script on failure. | |
# Displays the selected menu option, runs the command, and prints a warning if it fails. | |
# This is intended for batch sequences where individual step failures should not stop the whole process. | |
# Arguments: | |
# $1 - Numeric menu choice (used for display) | |
# $@ - Command and its arguments to run | |
run_soft_step() { | |
local choice="$1"; shift | |
printSelectedOption "$choice" | |
if ! "$@"; then | |
echo -e "⚠️ ${bldred}Error:${txtrst} Step [$1] failed" | |
fi | |
} | |
# Batch: Run all backup-related maintenance tasks in sequence. | |
run_all_backups_func() { | |
run_soft_step 1 dotfiles_func || echo "[dotfiles_func] failed" | |
run_soft_step 9 ports-infoBackup.sh || echo "[ports-infoBackup.sh] failed" | |
run_soft_step 13 brew_conf_export.sh || echo "[brew_conf_export.sh] failed" | |
run_soft_step 20 pip_backup_func || echo "[pip_backup_func] failed" | |
} | |
# Batch: Run all updates-related maintenance tasks in sequence. | |
run_all_updates_func() { | |
run_step 4 softwareupdate --list || echo "[softwareupdate] failed" | |
run_step 5 mas upgrade || echo "[mas upgrade] failed" | |
privileged_run_step 8 ports-update.sh APPLY || echo "[ports-update.sh APPLY] failed" | |
run_step 12 brew-update.sh || echo "[brew-update.sh] failed" | |
run_step 16 nix_upgrade_func || echo "[nix_upgrade_func] failed" | |
} | |
# Encapsulated run command | |
run_step() { | |
local choice="$1"; shift | |
printSelectedOption "$choice" | |
"$@" || { | |
fatal "Command failed. Aborting." | |
} | |
infoFinished | |
} | |
# Encapsulated command runner for privileged operations (with sudo warning) | |
privileged_run_step() { | |
local choice="$1"; shift | |
local script="$1"; shift | |
printSelectedOption "$choice" | |
checkTools "$script" | |
warningRootDisplay | |
sudo "$script" "$@" || { | |
fatal "Command failed. Aborting." | |
} | |
infoFinished | |
} | |
# __ __ _ | |
# | \/ |__ _(_)_ _ | |
# | |\/| / _` | | ' \ | |
# |_| |_\__,_|_|_||_| | |
# | |
# Initial system and environment checks | |
initChecks | |
# __ __ | |
# | \/ |___ _ _ _ _ | |
# | |\/| / -_) ' \ || | | |
# |_| |_\___|_||_\_,_| | |
# | |
# Menu options to be selected | |
OPTIONS=( | |
"-" "———— SYNC & DOTFILES ————" | |
1 "Sync dotfiles & prefs to Dropbox (mackup)" | |
2 "Compare ~/.bashrc.d with synced version" | |
3 "Update local Git repos (fast mode)" | |
"-" "" | |
"-" "———— SYSTEM & APP UPDATES ————" | |
4 "Check macOS system updates (apple)" | |
5 "Update Mac App Store apps (mas)" | |
6 "MacUpdater: Scan apps for updates (1st)" | |
7 "List available MacUpdater updates (2nd)" | |
"-" "" | |
"-" "———— MACPORTS ————" | |
8 "Update MacPorts packages" | |
9 "Backup MacPorts config to Dropbox" | |
10 "Compare MacPorts with reference env" | |
11 "List MacPorts with x86_64 binaries" | |
"-" "" | |
"-" "———— HOMEBREW ————" | |
12 "Update Homebrew packages" | |
13 "Backup Brew bundle to Dropbox" | |
14 "Compare Brew with reference env" | |
15 "List Brew formulae with x86_64 binaries" | |
"-" "" | |
"-" "———— NIX ————" | |
16 "Upgrade Nix environment" | |
17 "Activate Home Manager profile" | |
18 "Rollback Home Manager config" | |
"-" "" | |
"-" "———— PYTHON ENV ————" | |
19 "Set Python ${PYVERS_H} as default" | |
20 "Export pip freeze list to Dropbox" | |
"-" "" | |
"-" "———— MAINTENANCE & TOOLS ————" | |
21 "Rebuild Spotlight index" | |
22 "Reset Spotlight position (UI)" | |
23 "Remove Microsoft AutoUpdate" | |
24 "Check SSL certificate expirations" | |
25 "Sanitize shell configuration" | |
26 "Make sure Desktop only have aliases" | |
"-" "" | |
"-" "———— BATCH ————" | |
98 "Run all update tasks (Apple, Ports, Brew, Nix)" | |
99 "Run all backup tasks (dotfiles, pip, Brew, Ports)" | |
) | |
# Auto-select the tag number | |
DEFAULT_SELECTION="2" | |
# Dialog menu configuration (dynamic sizing, titles, labels, and prompts) | |
BACKTITLE="macOS System Maintenance - ${LOGNAME}" | |
TITLE="Available options to run on $(hostname -s)" | |
MENU="Choose one of the following options:" | |
OK_LABEL="Run" | |
CANCEL_LABEL="Exit" | |
TIMEOUT_SEC="50" | |
# Get current terminal size | |
TERM_HEIGHT=$(tput lines) | |
TERM_WIDTH=$(tput cols) | |
# Calculate dialog box size (percentage of terminal size) | |
HEIGHT=$(( TERM_HEIGHT * 70 / 100 )) # 70% of terminal height | |
WIDTH=$(( TERM_WIDTH * 40 / 100 )) # 40% of terminal width | |
# Enforce minimum dialog sizes | |
[ "$HEIGHT" -lt 10 ] && HEIGHT=10 | |
[ "$WIDTH" -lt 40 ] && WIDTH=40 | |
# Calculate CHOICE_HEIGHT based on number of options | |
TOTAL_OPTIONS=${#OPTIONS[@]} | |
# OPTIONS contains index/label pairs | |
TOTAL_ITEMS=$(( TOTAL_OPTIONS / 2 )) | |
# Dynamically adapt height based on terminal size and item count | |
# Leave room for borders and titles | |
MAX_MENU_HEIGHT=$(( TERM_HEIGHT - 10 )) | |
# Choose the smaller: total items or available space | |
CHOICE_HEIGHT=$(( TOTAL_ITEMS < MAX_MENU_HEIGHT ? TOTAL_ITEMS : MAX_MENU_HEIGHT )) | |
# Ensure a minimum number of items are visible in the menu | |
[ "$CHOICE_HEIGHT" -lt 5 ] && CHOICE_HEIGHT=5 | |
# This associative array creates a reliable link between the displayed ordered list (numeric choices) | |
# and internal named actions. It decouples the user-visible numbering from the script's logic, | |
# making the code easier to maintain if the menu order changes, without having to rewrite the case structure. | |
# (numeric menu ID → internal function or script name) | |
declare -A CHOICE_LABEL=( | |
# SYNC | |
[1]="dotfiles" | |
[2]="bashrc" | |
[3]="git_fetch" | |
# SYSTEM | |
[4]="apple" | |
[5]="mas" | |
[6]="macup_scan" | |
[7]="macup_list" | |
# MACPORTS | |
[8]="ports_up" | |
[9]="backup_ports" | |
[10]="compare_ports" | |
[11]="report_x86_ports" | |
# BREW | |
[12]="brew_up" | |
[13]="backup_brew" | |
[14]="compare_brew" | |
[15]="report_x86_brews" | |
# NIX | |
[16]="nix_upgrade" | |
[17]="nix_activate" | |
[18]="nix_rollback" | |
# PYTHON ENV | |
[19]="python_version" | |
[20]="pip_backup" | |
# MAINTENANCE | |
[21]="spotlight" | |
[22]="spotlight_reset" | |
[23]="remove_msupdt" | |
[24]="ssl_checks" | |
[25]="sanitize_shell_checks" | |
[26]="userDesktopNotAliases" | |
# BATCH | |
[98]="run_all_updates" | |
[99]="run_all_backups" | |
) | |
# Clear terminal before showing menu (avoids clutter) | |
clear | |
# | |
# _ __ __ _ _ _ ___ ___ __ _ _ _ __ _ ___ | |
# | '_ \/ _` | '_(_-</ -_) / _` | '_/ _` (_-< | |
# | .__/\__,_|_| /__/\___|_\__,_|_| \__, /__/ | |
# |_| |___| |___/ | |
# --- Init argument flags --- | |
DEBUG_CHOICES=0 | |
GUM_AVAILABLE=0 | |
FORCE_DIALOG=0 | |
NON_INTERACTIVE=0 | |
# --- First pass: Parse known global flags --- | |
for arg in "$@"; do | |
case "$arg" in | |
--debug-choices) DEBUG_CHOICES=1 ;; | |
--gum) GUM_AVAILABLE=1 ;; | |
--dialog) FORCE_DIALOG=1 ;; | |
esac | |
done | |
# --- Handle --debug-choices early --- | |
if [[ "$DEBUG_CHOICES" -eq 1 ]]; then | |
debug_validate_choice_map | |
exit 0 | |
fi | |
# --- Set GUM if not overridden --- | |
if [[ "$FORCE_DIALOG" -eq 1 ]]; then | |
GUM_AVAILABLE=0 | |
elif [[ "$GUM_AVAILABLE" -eq 0 && -x "$(command -v gum)" ]]; then | |
GUM_AVAILABLE=1 | |
GUM_HEADER="${TITLE} — ${MENU}" | |
fi | |
# --- Handle main actions --- | |
if [[ "$1" == "-h" || "$1" == "--help" ]]; then | |
usage | |
elif [[ "$1" == "--list" ]]; then | |
echo -e "\nAvailable actions:" | |
for id in "${!CHOICE_LABEL[@]}"; do | |
printf " %2d - %s\n" "$id" "${CHOICE_LABEL[$id]}" | |
done | sort -n | |
exit 0 | |
elif [[ "$1" == "--run" ]]; then | |
if [[ $# -ne 2 || ! "$2" =~ ^[0-9]+$ ]]; then | |
fatal "Missing or invalid argument for --run." | |
fi | |
CHOICE="$2" | |
echo -e "${bldblu}Running in non-interactive mode:${txtrst} Option $CHOICE" | |
[[ -z "${CHOICE_LABEL[$CHOICE]}" ]] && fatal "Invalid option ID." | |
NON_INTERACTIVE=1 | |
elif [[ $# -gt 0 && "$1" != "--gum" && "$1" != "--dialog" ]]; then | |
fatal "Unknown or excessive arguments." | |
fi | |
# Launch menu using gum or dialog | |
menu_cmd() { | |
if [[ "$GUM_AVAILABLE" -eq 1 ]]; then | |
if ! command -v gum >/dev/null 2>&1; then | |
echo -e "${bldred}Error:${txtrst} gum is not installed but was forced/enabled. Falling back to dialog." | |
GUM_AVAILABLE=0 | |
fi | |
fi | |
if [[ "$GUM_AVAILABLE" -eq 1 ]]; then | |
mapfile -t choices < <(sanitize_gum_choices) | |
# Get default selection label | |
# Extract the full "label" for the default selection (e.g. "2 - Sync dotfiles") | |
DEFAULT_SELECTION_LABEL="" | |
for item in "${choices[@]}"; do | |
if [[ "$item" =~ ^${DEFAULT_SELECTION}[[:space:]]+- ]]; then | |
DEFAULT_SELECTION_LABEL="$item" | |
break | |
fi | |
done | |
# Use gum choose (multi-select) | |
mapfile -t CHOICE_RAW < <(gum choose \ | |
--height="$CHOICE_HEIGHT" \ | |
--header="$GUM_HEADER" \ | |
--timeout="${TIMEOUT_SEC}s" \ | |
--limit=1 \ | |
--selected "$DEFAULT_SELECTION_LABEL" \ | |
"${choices[@]}") | |
DIALOG_EXIT_CODE=$? | |
if [[ $DIALOG_EXIT_CODE -eq 130 || ${#CHOICE_RAW[@]} -eq 0 ]]; then | |
clear | |
echo -e "\n${bldbrw}User aborted.${txtrst} Exiting." | |
exit 0 | |
fi | |
# Extract numeric tags | |
CHOICES=() | |
for line in "${CHOICE_RAW[@]}"; do | |
if [[ "$line" =~ ^([0-9]+)[[:space:]]+- ]]; then | |
CHOICES+=("${BASH_REMATCH[1]}") | |
fi | |
done | |
if [[ ${#CHOICES[@]} -eq 0 ]]; then | |
fatal "Invalid or non-actionable menu selection. Exiting." | |
fi | |
# Return list of tags | |
printf "%s\n" "${CHOICES[@]}" | |
else | |
# Display the interactive dialog menu and store the user's selection in CHOICE. | |
# shellcheck disable=SC2069 | |
dialog --clear \ | |
--colors \ | |
--backtitle "$BACKTITLE" \ | |
--title "$TITLE" \ | |
--ok-label "$OK_LABEL" \ | |
--default-item "$DEFAULT_SELECTION" \ | |
--cancel-label "$CANCEL_LABEL" \ | |
--timeout $TIMEOUT_SEC \ | |
--menu "$MENU" \ | |
$HEIGHT $WIDTH $CHOICE_HEIGHT \ | |
"${OPTIONS[@]}" \ | |
2>&1 >/dev/tty | |
fi | |
} | |
# Non-interactive mode (e.g. ./refresh_system.sh --run 13) | |
if [[ "$NON_INTERACTIVE" != "1" ]]; then | |
CHOICE=$(menu_cmd) | |
DIALOG_EXIT_CODE=$? | |
fi | |
# Clean up dialog screen if used | |
if [[ "$GUM_AVAILABLE" -eq 0 ]]; then | |
clear | |
fi | |
# Handle empty or cancelled gum/menu selection | |
if [[ $DIALOG_EXIT_CODE -ne 0 || -z "$CHOICE" ]]; then | |
if [[ "$DIALOG_EXIT_CODE" -eq 130 ]]; then | |
printf "\n${bldbrw}User aborted.${txtrst} Exiting.\n" | |
elif [[ "$DIALOG_EXIT_CODE" -eq 0 ]]; then | |
printf "\n${bldbrw}No actionable option selected.${txtrst} Exiting.\n" | |
else | |
printf "\n${bldbrw}Menu exited with code $DIALOG_EXIT_CODE.${txtrst} Exiting.\n" | |
fi | |
exit 0 | |
fi | |
# _ _ | |
# | | __ _ _ _ _ _ __| |_ ___ _ _ | |
# | |__/ _` | || | ' \/ _| ' \/ -_) '_| | |
# |____\__,_|\_,_|_||_\__|_||_\___|_| | |
# | |
# Map the selected menu number to its corresponding action name and execute the matching case block. | |
if ! [[ "$CHOICE" =~ ^[0-9]+$ ]] || [ -z "${CHOICE_LABEL[$CHOICE]}" ]; then | |
fatal "Invalid or non-actionable menu selection. Exiting." | |
fi | |
CHOICE_NAME="${CHOICE_LABEL[$CHOICE]}" | |
case $CHOICE_NAME in | |
spotlight_reset) | |
run_step "$CHOICE" reset_spotlight_position.sh | |
;; | |
compare_ports) | |
run_step "$CHOICE" compare_ports_home_office.sh | |
;; | |
compare_brew) | |
run_step "$CHOICE" compare_brew_home_office.sh | |
;; | |
brew_up) | |
run_step "$CHOICE" brew-update.sh | |
;; | |
backup_ports) | |
run_step "$CHOICE" ports-infoBackup.sh | |
;; | |
backup_brew) | |
run_step "$CHOICE" brew_conf_export.sh | |
;; | |
bashrc) | |
run_step "$CHOICE" bash_rc_reseync_v2.sh | |
;; | |
ssl_checks) | |
run_step "$CHOICE" certs_validity.sh | |
;; | |
sanitize_shell_checks) | |
run_step "$CHOICE" check_env_shell.sh | |
;; | |
nix_activate) | |
run_step "$CHOICE" nix-manager-adm activate | |
;; | |
nix_rollback) | |
run_step "$CHOICE" nix-manager-adm rollback | |
;; | |
report_x86_ports) | |
run_step "$CHOICE" report_x86_ports_func | |
;; | |
report_x86_brews) | |
run_step "$CHOICE" report_x86_brews_func | |
;; | |
spotlight) | |
run_step "$CHOICE" rebuild_spotlight_func | |
;; | |
nix_upgrade) | |
run_step "$CHOICE" nix_upgrade_func | |
;; | |
pip_backup) | |
run_step "$CHOICE" pip_backup_func | |
;; | |
macup_scan) | |
run_step "$CHOICE" macup_scan_func | |
;; | |
macup_list) | |
run_step "$CHOICE" macup_list_func | |
;; | |
userDesktopNotAliases) | |
run_step "$CHOICE" userDesktopNotAliases_func | |
;; | |
git_fetch) | |
run_step "$CHOICE" git_fetch_func | |
;; | |
dotfiles) | |
run_step "$CHOICE" dotfiles_func | |
;; | |
run_all_backups) | |
run_step "$CHOICE" run_all_backups_func | |
;; | |
run_all_updates) | |
run_step "$CHOICE" run_all_updates_func | |
;; | |
mas) | |
run_step "$CHOICE" mas upgrade | |
;; | |
apple) | |
run_step "$CHOICE" softwareupdate --list | |
;; | |
remove_msupdt) | |
privileged_run_step "$CHOICE" remove-MAU2.sh | |
;; | |
python_version) | |
privileged_run_step "$CHOICE" set-python-123.sh "${PYVERS}" | |
;; | |
ports_up) | |
privileged_run_step "$CHOICE" ports-update.sh APPLY | |
;; | |
*) | |
fatal "Unknown option [ $CHOICE ] selected. This should never happen." | |
;; | |
esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment