MACSTARTER: interactive Mac setup script (brew/cask/mas, tweaks, keys, Tailscale)

Ultrabunt-style dialog checklist installer for fresh Intel/Apple Silicon Macs.
Bootstraps CLT + Homebrew, then categorized menus: core CLI, dev/AI CLIs,
network, media, Android, full Xcode, GUI apps, macOS tweaks, SSH/Tailscale
key setup, and the legacy macpress WordPress stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-17 14:58:30 +10:00
parent 6de818ed53
commit dbd58ff11e
2 changed files with 688 additions and 2 deletions

View File

@ -1,2 +1,58 @@
# macpress
fast lamp/lemp for intel/arm macs
# 🍎 MACSTARTER
*Fresh Mac → fully loaded, without pissfarting around in terminal for hours.*
Little sibling of [UltraBunt](https://github.com/type-two/ultrabunt). Was once a fast
LAMP/WordPress installer for Intel & Apple Silicon Macs — now it's a full first-time
Mac setup script with UltraBunt-style checklist menus. (The WordPress bit is still
in there as a category, for old times' sake.)
## 🚀 Quick start
On a brand new Mac (Apple Silicon or Intel):
```bash
git clone https://gitea.partly.party/monster/MACSTARTER.git
cd MACSTARTER
./macstarter.sh
```
That's it. The script bootstraps everything it needs itself:
Xcode Command Line Tools → Homebrew → menus.
Pick **FULL SEND** from the main menu to install every recommended item in one go,
or dive into categories and tick exactly what you want. Safe to re-run —
everything checks before it installs.
```bash
./macstarter.sh --catalog # just print everything it can install
```
## 🎪 Categories
| Category | Highlights |
|---|---|
| **Core CLI tools** | git, jq, ripgrep, fzf, htop, btop, real rsync, bat, eza, tmux… |
| **Dev tools & AI CLIs** | node, GitHub CLI, Claude Code, Antigravity CLI/IDE |
| **Network & sync** | Tailscale, rclone, nmap, mtr |
| **Media** | ffmpeg, yt-dlp, imagemagick, VLC |
| **Android** | adb + fastboot (platform-tools), scrcpy |
| **Apple dev** | Full Xcode via App Store (license accept + first-run handled) |
| **GUI apps** | Brave, Warp, Raycast, Rectangle… |
| **macOS tweaks** | natural scrolling OFF, show extensions/hidden files, fast key repeat, screenshots to ~/Screenshots, no .DS_Store litter |
| **Keys & Tailscale network** | ed25519 SSH key + keychain agent, ~/.ssh/config defaults, Tailscale login/join, CLI alias |
| **WordPress (macpress classic)** | php, mariadb, wp-cli, one-shot local WP site |
## 📝 Notes
- Full Xcode needs you signed in to the App Store first (it's ~12GB, go make a coffee).
- Tailscale join opens the menu bar app for login — the script picks up once you're in.
- The SSH key step copies your new public key to the clipboard, ready to paste into
your tailnet boxes or GitHub.
- Scrolling/keyboard tweaks fully apply after you log out and back in.
- Everything logs to `~/macstarter-install.log`.
## ✅ Install policy
Apps install via `brew` (formulae + casks) or `mas` (App Store) only.
No curl-pipe-bash randomness beyond Homebrew's own installer.

630
macstarter.sh Executable file
View File

@ -0,0 +1,630 @@
#!/bin/bash
# ==============================================================================
#
# ███╗ ███╗ █████╗ ██████╗███████╗████████╗ █████╗ ██████╗ ████████╗███████╗██████╗
# ████╗ ████║██╔══██╗██╔════╝██╔════╝╚══██╔══╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
# ██╔████╔██║███████║██║ ███████╗ ██║ ███████║██████╔╝ ██║ █████╗ ██████╔╝
# ██║╚██╔╝██║██╔══██║██║ ╚════██║ ██║ ██╔══██║██╔══██╗ ██║ ██╔══╝ ██╔══██╗
# ██║ ╚═╝ ██║██║ ██║╚██████╗███████║ ██║ ██║ ██║██║ ██║ ██║ ███████╗██║ ██║
# ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
#
# MACSTARTER — fast first-time setup for Intel & Apple Silicon Macs
# Little sibling of UltraBunt, grew out of macpress. Menus, categories,
# zero ball-ache.
#
# Usage: ./macstarter.sh interactive menus
# ./macstarter.sh --catalog print the app catalog and exit
#
# Safe to re-run: everything checks before it installs.
# ==============================================================================
# ------------------------------------------------------------------------------
# Colors & logging
# ------------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m'
LOGFILE="$HOME/macstarter-install.log"
log() {
echo -e "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOGFILE"
echo -e "$*"
}
ok() { log "${GREEN}${NC} $*"; }
warn() { log "${YELLOW}${NC} $*"; }
err() { log "${RED}${NC} $*"; }
RESULTS_OK=()
RESULTS_FAIL=()
RESULTS_SKIP=()
# ------------------------------------------------------------------------------
# THE CATALOG
# Format: tag|TYPE|payload|default(on/off)|Description
# TYPE: BREW (formula) | CASK | MAS (App Store id) | CUSTOM (function name)
# ------------------------------------------------------------------------------
CAT_CORE=(
"git|BREW|git|on|Version control"
"wget|BREW|wget|on|Network downloader"
"jq|BREW|jq|on|JSON processor"
"ripgrep|BREW|ripgrep|on|Fast text search (rg)"
"fd|BREW|fd|on|Modern find"
"fzf|BREW|fzf|on|Fuzzy finder"
"tree|BREW|tree|on|Directory visualizer"
"tmux|BREW|tmux|on|Terminal multiplexer"
"ncdu|BREW|ncdu|on|Disk usage analyzer"
"bat|BREW|bat|on|Cat with syntax highlighting"
"eza|BREW|eza|on|Modern ls"
"tldr|BREW|tldr|on|Simplified man pages"
"htop|BREW|htop|on|Process viewer"
"btop|BREW|btop|on|Prettier process viewer"
"rsync|BREW|rsync|on|Real rsync (macOS ships a crusty old one)"
"watch|BREW|watch|off|Re-run a command on interval"
"coreutils|BREW|coreutils|off|GNU coreutils (gls, gdate, etc)"
)
CAT_DEV=(
"node|BREW|node|on|Node.js + npm"
"gh|BREW|gh|on|GitHub CLI"
"python|BREW|python|off|Python 3 via brew"
"go|BREW|go|off|Go language"
"claude-code|CASK|claude-code|on|Claude Code — terminal AI coding"
"antigravity-cli|CASK|antigravity-cli|on|Google Antigravity CLI"
"antigravity|CASK|antigravity|off|Google Antigravity IDE"
"vscode|CASK|visual-studio-code|off|Visual Studio Code"
"lazygit|BREW|lazygit|off|Terminal UI for git"
"shellcheck|BREW|shellcheck|off|Shell script linter"
)
CAT_NET=(
"tailscale|CASK|tailscale-app|on|Tailscale mesh VPN (GUI app + CLI)"
"rclone|BREW|rclone|on|Sync to/from cloud storage"
"nmap|BREW|nmap|off|Network scanner"
"mtr|BREW|mtr|off|Traceroute + ping combo"
"speedtest|BREW|speedtest-cli|off|Internet speed test"
"wireguard|BREW|wireguard-tools|off|WireGuard CLI tools"
)
CAT_MEDIA=(
"ffmpeg|BREW|ffmpeg|on|Swiss-army media converter"
"yt-dlp|BREW|yt-dlp|off|Video downloader"
"imagemagick|BREW|imagemagick|off|Image conversion toolkit"
"exiftool|BREW|exiftool|off|Read/write media metadata"
"vlc|CASK|vlc|off|Plays literally anything"
)
CAT_ANDROID=(
"platform-tools|CASK|android-platform-tools|on|adb + fastboot"
"scrcpy|BREW|scrcpy|on|Mirror & control Android over adb"
)
CAT_APPLE=(
"xcode|CUSTOM|install_full_xcode|on|Full Xcode IDE via App Store (~12GB, needs sign-in)"
"xcodes|BREW|xcodesorg/made/xcodes|off|Manage multiple Xcode versions"
"swiftlint|BREW|swiftlint|off|Swift linter"
)
CAT_APPS=(
"brave|CASK|brave-browser|on|Brave browser"
"warp|CASK|warp|on|Warp terminal"
"raycast|CASK|raycast|off|Launcher on steroids"
"rectangle|CASK|rectangle|off|Window snapping"
"the-unarchiver|CASK|the-unarchiver|off|Opens every archive format"
"stats|CASK|stats|off|Menubar system monitor"
"keka|CASK|keka|off|Archiver / compressor"
)
CAT_TWEAKS=(
"scroll-off|CUSTOM|tweak_scroll_off|on|Turn OFF natural scrolling"
"show-ext|CUSTOM|tweak_show_extensions|on|Finder: always show file extensions"
"show-hidden|CUSTOM|tweak_show_hidden|on|Finder: show hidden files"
"finder-bars|CUSTOM|tweak_finder_bars|on|Finder: path bar + status bar"
"fast-keys|CUSTOM|tweak_fast_keys|on|Fast key repeat, no press-and-hold"
"screenshots|CUSTOM|tweak_screenshots_dir|on|Screenshots to ~/Screenshots"
"dock-autohide|CUSTOM|tweak_dock_autohide|off|Auto-hide the Dock"
"no-ds-store|CUSTOM|tweak_no_ds_store|on|No .DS_Store on network/USB drives"
)
CAT_KEYS=(
"ssh-key|CUSTOM|setup_ssh_key|on|Generate ed25519 SSH key + keychain agent"
"ssh-config|CUSTOM|setup_ssh_config|on|~/.ssh/config with keychain defaults"
"tailscale-join|CUSTOM|tailscale_join|on|Log in to Tailscale + join your tailnet"
"tailscale-alias|CUSTOM|tailscale_alias|on|Add 'tailscale' CLI alias to ~/.zshrc"
)
CAT_WORDPRESS=(
"php|BREW|php|on|PHP"
"mariadb|BREW|mariadb|on|MariaDB (MySQL)"
"wp-cli|BREW|wp-cli|on|WordPress CLI"
"wp-site|CUSTOM|wp_quick_site|off|Spin up a local WP site right now"
)
CATEGORIES=(
"core|Core CLI tools|CAT_CORE"
"dev|Dev tools & AI CLIs|CAT_DEV"
"net|Network & sync|CAT_NET"
"media|Media|CAT_MEDIA"
"android|Android|CAT_ANDROID"
"apple|Apple dev|CAT_APPLE"
"apps|GUI apps|CAT_APPS"
"tweaks|macOS tweaks|CAT_TWEAKS"
"keys|Keys & Tailscale network|CAT_KEYS"
"wordpress|WordPress (macpress classic)|CAT_WORDPRESS"
)
# ------------------------------------------------------------------------------
# Catalog dump (non-interactive, for a quick look / sanity check)
# ------------------------------------------------------------------------------
print_catalog() {
local cat_entry cat_title cat_var items entry tag type payload def desc
for cat_entry in "${CATEGORIES[@]}"; do
IFS='|' read -r _ cat_title cat_var <<< "$cat_entry"
echo ""
echo "== $cat_title =="
eval "items=(\"\${${cat_var}[@]}\")"
for entry in "${items[@]}"; do
IFS='|' read -r tag type payload def desc <<< "$entry"
printf " %-16s %-7s %-28s [%s] %s\n" "$tag" "$type" "$payload" "$def" "$desc"
done
done
}
if [[ "${1:-}" == "--catalog" ]]; then
print_catalog
exit 0
fi
# ------------------------------------------------------------------------------
# Bootstrap: CLT -> Homebrew -> dialog + mas
# Runs every launch; every step is a no-op if already done.
# ------------------------------------------------------------------------------
BREW_PREFIX="/opt/homebrew"
[[ "$(uname -m)" == "x86_64" ]] && BREW_PREFIX="/usr/local"
bootstrap() {
echo -e "${CYAN}── macstarter bootstrap ──${NC}"
# Xcode Command Line Tools (Homebrew needs these)
if ! xcode-select -p &>/dev/null; then
warn "Xcode Command Line Tools missing — triggering install dialog..."
xcode-select --install 2>/dev/null
echo -e "${WHITE}Click 'Install' in the popup, then wait. I'll poll until it's done.${NC}"
until xcode-select -p &>/dev/null; do sleep 10; done
ok "Command Line Tools installed"
else
ok "Command Line Tools present"
fi
# Homebrew
if [[ -x "$BREW_PREFIX/bin/brew" ]]; then
ok "Homebrew present"
else
warn "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || {
err "Homebrew install failed — cannot continue"; exit 1; }
ok "Homebrew installed"
fi
eval "$("$BREW_PREFIX/bin/brew" shellenv)"
# Make sure future shells get brew too
if ! grep -q 'brew shellenv' "$HOME/.zprofile" 2>/dev/null; then
echo "eval \"\$($BREW_PREFIX/bin/brew shellenv)\"" >> "$HOME/.zprofile"
ok "Added brew shellenv to ~/.zprofile"
fi
# dialog drives the menus, mas drives the App Store
for tool in dialog mas; do
if ! command -v "$tool" &>/dev/null; then
warn "Installing $tool..."
brew install "$tool" >>"$LOGFILE" 2>&1 && ok "$tool installed" || err "$tool failed"
fi
done
}
# ------------------------------------------------------------------------------
# dialog helpers
# ------------------------------------------------------------------------------
ui_msg() {
dialog --title "$1" --msgbox "$2" 14 72
}
ui_yesno() {
dialog --title "$1" --yesno "$2" 12 72
}
# ------------------------------------------------------------------------------
# Installers
# ------------------------------------------------------------------------------
install_formula() {
local name="$1"
if brew list --formula "$name" &>/dev/null; then
ok "$name already installed (formula)"
RESULTS_SKIP+=("$name")
elif brew install "$name" >>"$LOGFILE" 2>&1; then
ok "$name installed"
RESULTS_OK+=("$name")
else
err "$name FAILED (see $LOGFILE)"
RESULTS_FAIL+=("$name")
fi
}
install_cask() {
local name="$1"
if brew list --cask "$name" &>/dev/null; then
ok "$name already installed (cask)"
RESULTS_SKIP+=("$name")
elif brew install --cask "$name" >>"$LOGFILE" 2>&1; then
ok "$name installed"
RESULTS_OK+=("$name")
else
err "$name FAILED (see $LOGFILE)"
RESULTS_FAIL+=("$name")
fi
}
install_mas() {
local app_id="$1" name="$2"
if mas list 2>/dev/null | grep -q "^$app_id "; then
ok "$name already installed (App Store)"
RESULTS_SKIP+=("$name")
elif mas install "$app_id" >>"$LOGFILE" 2>&1; then
ok "$name installed from App Store"
RESULTS_OK+=("$name")
else
err "$name FAILED — are you signed in to the App Store? (open it, sign in, retry)"
RESULTS_FAIL+=("$name")
fi
}
run_custom() {
local fn="$1" label="$2"
if "$fn"; then
RESULTS_OK+=("$label")
else
RESULTS_FAIL+=("$label")
fi
}
dispatch() {
local entry="$1" tag type payload def desc
IFS='|' read -r tag type payload def desc <<< "$entry"
echo ""
log "${CYAN}── $tag: $desc ──${NC}"
case "$type" in
BREW) install_formula "$payload" ;;
CASK) install_cask "$payload" ;;
MAS) install_mas "$payload" "$tag" ;;
CUSTOM) run_custom "$payload" "$tag" ;;
esac
}
# ------------------------------------------------------------------------------
# CUSTOM: Apple dev
# ------------------------------------------------------------------------------
install_full_xcode() {
if [[ -d "/Applications/Xcode.app" ]]; then
ok "Xcode already installed"
return 0
fi
warn "Installing full Xcode from the App Store — this is a ~12GB download."
warn "You must be signed in to the App Store (open it and sign in if this fails)."
if mas install 497799835 >>"$LOGFILE" 2>&1; then
ok "Xcode downloaded"
warn "Accepting license + first-run setup (needs sudo)..."
sudo xcodebuild -license accept >>"$LOGFILE" 2>&1 || true
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer >>"$LOGFILE" 2>&1 || true
xcodebuild -runFirstLaunch >>"$LOGFILE" 2>&1 || true
ok "Xcode ready"
return 0
fi
err "Xcode install failed — sign in to the App Store app, then re-run this item"
return 1
}
# ------------------------------------------------------------------------------
# CUSTOM: macOS tweaks
# ------------------------------------------------------------------------------
tweak_scroll_off() {
defaults write NSGlobalDomain com.apple.swipescrolldirection -bool false
ok "Natural scrolling OFF (log out/in for it to stick everywhere)"
}
tweak_show_extensions() {
defaults write NSGlobalDomain AppleShowAllExtensions -bool true
NEED_FINDER_RESTART=1
ok "File extensions always shown"
}
tweak_show_hidden() {
defaults write com.apple.finder AppleShowAllFiles -bool true
NEED_FINDER_RESTART=1
ok "Hidden files shown in Finder"
}
tweak_finder_bars() {
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true
NEED_FINDER_RESTART=1
ok "Finder path bar + status bar on"
}
tweak_fast_keys() {
defaults write NSGlobalDomain KeyRepeat -int 2
defaults write NSGlobalDomain InitialKeyRepeat -int 15
defaults write NSGlobalDomain ApplePressAndHoldEnabled -bool false
ok "Fast key repeat, press-and-hold disabled (log out/in to apply)"
}
tweak_screenshots_dir() {
mkdir -p "$HOME/Screenshots"
defaults write com.apple.screencapture location -string "$HOME/Screenshots"
killall SystemUIServer 2>/dev/null || true
ok "Screenshots now land in ~/Screenshots"
}
tweak_dock_autohide() {
defaults write com.apple.dock autohide -bool true
killall Dock 2>/dev/null || true
ok "Dock auto-hides"
}
tweak_no_ds_store() {
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
ok "No more .DS_Store litter on network/USB drives"
}
# ------------------------------------------------------------------------------
# CUSTOM: Keys & Tailscale network
# ------------------------------------------------------------------------------
SSH_KEY="$HOME/.ssh/id_ed25519"
setup_ssh_key() {
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
if [[ -f "$SSH_KEY" ]]; then
ok "SSH key already exists: $SSH_KEY"
else
ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -C "$USER@$(scutil --get LocalHostName 2>/dev/null || hostname -s)" >>"$LOGFILE" 2>&1
ok "Generated ed25519 key: $SSH_KEY"
fi
ssh-add --apple-use-keychain "$SSH_KEY" >>"$LOGFILE" 2>&1 && ok "Key added to keychain agent"
echo ""
echo -e "${WHITE}Your public key — paste this into ~/.ssh/authorized_keys on your tailnet boxes"
echo -e "(or GitHub → Settings → SSH keys):${NC}"
echo ""
cat "$SSH_KEY.pub"
echo ""
pbcopy < "$SSH_KEY.pub" && ok "Public key copied to clipboard"
return 0
}
setup_ssh_config() {
local cfg="$HOME/.ssh/config"
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
if [[ -f "$cfg" ]] && grep -q "UseKeychain" "$cfg"; then
ok "~/.ssh/config already has keychain defaults"
return 0
fi
cat >> "$cfg" << 'EOF'
# macstarter defaults
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
EOF
chmod 600 "$cfg"
ok "Wrote keychain + keepalive defaults to ~/.ssh/config"
}
TAILSCALE_CLI="/Applications/Tailscale.app/Contents/MacOS/Tailscale"
tailscale_join() {
if [[ ! -d "/Applications/Tailscale.app" ]]; then
err "Tailscale app not installed — install it from the Network & sync menu first"
return 1
fi
open -a Tailscale
if "$TAILSCALE_CLI" status &>/dev/null; then
ok "Already connected to your tailnet:"
"$TAILSCALE_CLI" status | head -10
return 0
fi
echo ""
echo -e "${WHITE}Tailscale is open in your menu bar. Log in with your usual account"
echo -e "to join your tailnet, then press Enter here.${NC}"
read -r
if "$TAILSCALE_CLI" status &>/dev/null; then
ok "Connected! Your tailnet:"
"$TAILSCALE_CLI" status | head -10
return 0
fi
warn "Not connected yet — finish login in the menu bar app, it'll join automatically"
return 0
}
tailscale_alias() {
if [[ ! -d "/Applications/Tailscale.app" ]]; then
err "Tailscale app not installed yet"
return 1
fi
if grep -q "alias tailscale=" "$HOME/.zshrc" 2>/dev/null; then
ok "tailscale alias already in ~/.zshrc"
return 0
fi
echo "alias tailscale=\"$TAILSCALE_CLI\"" >> "$HOME/.zshrc"
ok "Added tailscale CLI alias to ~/.zshrc (new shells can run: tailscale status)"
}
# ------------------------------------------------------------------------------
# CUSTOM: macpress classic — a local WordPress in about a minute
# ------------------------------------------------------------------------------
wp_quick_site() {
for need in php mariadb wp-cli; do
if ! brew list --formula "$need" &>/dev/null; then
err "$need not installed — tick it in the WordPress menu first"
return 1
fi
done
local site_dir="$HOME/Sites/wp"
local db_name="wp_local"
warn "Setting up a local WordPress at $site_dir ..."
brew services start mariadb >>"$LOGFILE" 2>&1
sleep 3
local mysql_cmd="mariadb"
command -v mariadb &>/dev/null || mysql_cmd="mysql"
"$mysql_cmd" -u root -e "CREATE DATABASE IF NOT EXISTS $db_name" 2>>"$LOGFILE" || {
err "Couldn't create database — check 'brew services info mariadb'"
return 1
}
mkdir -p "$site_dir" && cd "$site_dir" || return 1
if [[ ! -f wp-settings.php ]]; then
wp core download >>"$LOGFILE" 2>&1 || { err "WP download failed"; return 1; }
fi
if [[ ! -f wp-config.php ]]; then
wp config create --dbname="$db_name" --dbuser=root --dbpass="" >>"$LOGFILE" 2>&1
fi
if ! wp core is-installed &>/dev/null; then
wp core install --url="http://localhost:8080" --title="macstarter" \
--admin_user=admin --admin_password=macstarter \
--admin_email="admin@localhost.local" --skip-email >>"$LOGFILE" 2>&1
fi
ok "WordPress ready at $site_dir"
echo ""
echo -e "${WHITE}Start it any time with: cd ~/Sites/wp && wp server --port=8080"
echo -e "Admin: http://localhost:8080/wp-admin (admin / macstarter — change it!)${NC}"
cd - >/dev/null || true
}
# ------------------------------------------------------------------------------
# Menus
# ------------------------------------------------------------------------------
NEED_FINDER_RESTART=0
run_entries_by_tags() {
# $1 = category array name, $2 = space-separated chosen tags
local cat_var="$1" chosen="$2" items entry tag
eval "items=(\"\${${cat_var}[@]}\")"
for entry in "${items[@]}"; do
tag="${entry%%|*}"
for c in $chosen; do
c="${c%\"}"; c="${c#\"}"
[[ "$c" == "$tag" ]] && dispatch "$entry"
done
done
}
category_checklist() {
# $1 = category array name, $2 = title
local cat_var="$1" title="$2" items entry tag type payload def desc args=() chosen
eval "items=(\"\${${cat_var}[@]}\")"
for entry in "${items[@]}"; do
IFS='|' read -r tag type payload def desc <<< "$entry"
args+=("$tag" "$desc" "$def")
done
chosen=$(dialog --title "$title" --checklist \
"Space toggles, Enter installs. Pre-ticked = recommended." \
22 76 14 "${args[@]}" 3>&1 1>&2 2>&3) || return 0
clear
[[ -z "$chosen" ]] && return 0
run_entries_by_tags "$cat_var" "$chosen"
echo ""
read -r -p "Done. Press Enter to go back to the menu..."
}
install_all_defaults() {
ui_yesno "FULL SEND" "Install every pre-ticked (recommended) item across ALL categories?\n\nThis is the whole M4 setup in one go. Xcode alone is ~12GB.\nTailscale login and SSH key steps will prompt when they need you." || { clear; return 0; }
clear
local cat_entry cat_var items entry def
for cat_entry in "${CATEGORIES[@]}"; do
IFS='|' read -r _ _ cat_var <<< "$cat_entry"
eval "items=(\"\${${cat_var}[@]}\")"
for entry in "${items[@]}"; do
IFS='|' read -r _ _ _ def _ <<< "$entry"
[[ "$def" == "on" ]] && dispatch "$entry"
done
done
echo ""
read -r -p "Full send complete. Press Enter for the summary..."
show_summary
}
show_summary() {
local msg=""
msg+="Installed: ${#RESULTS_OK[@]}\n"
msg+="Skipped (already there): ${#RESULTS_SKIP[@]}\n"
msg+="Failed: ${#RESULTS_FAIL[@]}\n"
if [[ ${#RESULTS_FAIL[@]} -gt 0 ]]; then
msg+="\nFailures:\n"
local f; for f in "${RESULTS_FAIL[@]}"; do msg+="$f\n"; done
msg+="\nDetails in $LOGFILE"
fi
ui_msg "SUMMARY" "$msg"
clear
}
finish_up() {
if [[ "$NEED_FINDER_RESTART" == "1" ]]; then
killall Finder 2>/dev/null || true
fi
clear
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
echo -e "${WHITE} macstarter done.${NC}"
echo -e " Installed: ${GREEN}${#RESULTS_OK[@]}${NC} Already there: ${YELLOW}${#RESULTS_SKIP[@]}${NC} Failed: ${RED}${#RESULTS_FAIL[@]}${NC}"
if [[ ${#RESULTS_FAIL[@]} -gt 0 ]]; then
echo -e " ${RED}Failed:${NC} ${RESULTS_FAIL[*]}"
fi
echo -e " Log: $LOGFILE"
echo ""
echo -e " ${YELLOW}Heads-up:${NC} scrolling & keyboard tweaks fully apply after you log out/in."
echo -e " Open a new terminal (or Warp, once you're in it) to pick up brew + aliases."
echo -e "${GREEN}════════════════════════════════════════════════════${NC}"
exit 0
}
main_menu() {
while true; do
local args=("all" ">>> FULL SEND — install all recommended <<<")
local cat_entry cat_key cat_title
for cat_entry in "${CATEGORIES[@]}"; do
IFS='|' read -r cat_key cat_title _ <<< "$cat_entry"
args+=("$cat_key" "$cat_title")
done
args+=("summary" "Show install summary")
args+=("quit" "Finish & exit")
local choice
choice=$(dialog --title "MACSTARTER" --menu \
"Fresh Mac to fully-loaded. Pick a category:" \
22 70 14 "${args[@]}" 3>&1 1>&2 2>&3) || finish_up
clear
case "$choice" in
all) install_all_defaults ;;
summary) show_summary ;;
quit) finish_up ;;
*)
for cat_entry in "${CATEGORIES[@]}"; do
IFS='|' read -r cat_key cat_title cat_var <<< "$cat_entry"
if [[ "$choice" == "$cat_key" ]]; then
category_checklist "$cat_var" "$cat_title"
fi
done
;;
esac
done
}
# ------------------------------------------------------------------------------
# GO
# ------------------------------------------------------------------------------
echo "macstarter run started $(date)" >> "$LOGFILE"
bootstrap
main_menu