Skip to content

Instantly share code, notes, and snippets.

@dzogrim
Last active September 11, 2025 08:42
Show Gist options
  • Save dzogrim/59481f6ef0296dcba0f59a0bfc53c1d7 to your computer and use it in GitHub Desktop.
Save dzogrim/59481f6ef0296dcba0f59a0bfc53c1d7 to your computer and use it in GitHub Desktop.
Provides an interactive system maintenance menu for macOS using 'dialog'
#!/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