Last active
May 22, 2025 15:37
-
-
Save erdaltoprak/df9959b0610a56d782d5fb636403b660 to your computer and use it in GitHub Desktop.
Automated macOS setup with homebrew, system & shell configuration
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 | |
############################################################################### | |
# Mac Bootstrap – Preferences, Homebrew, MAS apps | |
# Works with the stock /bin/bash 3.2 on macOS (no mapfile required) | |
# (c) 2025 Erdal • MIT License | |
############################################################################### | |
############################################################################### | |
# Colours / log helpers | |
############################################################################### | |
GREEN=$'\033[0;32m'; BLUE=$'\033[0;34m' | |
RED=$'\033[0;31m'; YELLOW=$'\033[1;33m'; NC=$'\033[0m'; BOLD=$'\033[1m' | |
log_info() { printf '%sℹ️ %s%s\n' "$BLUE" "$1" "$NC"; } | |
log_success() { printf '%s✅ %s%s\n' "$GREEN" "$1" "$NC"; } | |
log_warning() { printf '%s⚠️ %s%s\n' "$YELLOW" "$1" "$NC"; } | |
log_error() { printf '%s❌ %s%s\n' "$RED" "$1" "$NC"; } | |
############################################################################### | |
# Robust shell behaviour & cleanup | |
############################################################################### | |
set -Eeuo pipefail | |
cleanup() { | |
tput cnorm || true | |
} | |
trap cleanup EXIT | |
trap 'log_error "Interrupted"; exit 1' INT HUP TERM | |
trap 'log_error "Line $LINENO (exit $?) – $BASH_COMMAND"; exit 1' ERR | |
############################################################################### | |
# Progress-bar helpers | |
############################################################################### | |
BAR_W=30 | |
show_bar() { # no trailing newline | |
local pct=$1 msg=$2 | |
local done=$(( BAR_W * pct / 100 )) | |
local todo=$(( BAR_W - done )) | |
printf '\r\033[K%s┃%s' "$BLUE" "$NC" | |
printf '%*s' "$done" '' | tr ' ' '█' | |
printf '%*s' "$todo" '' | tr ' ' '░' | |
printf '%s┃ %3d%% %s%s' "$BLUE" "$pct" "$msg" "$NC" | |
} | |
newline_below_bar() { printf '\n'; } | |
############################################################################### | |
# Welcome | |
############################################################################### | |
welcome() { | |
echo "======================================================" | |
log_info "🎯 Mac Setup Script – Preferences & Apps" | |
echo "======================================================" | |
log_warning "You'll be prompted for your password when needed." | |
echo "1. Sign in to the Mac App Store" | |
echo "2. Review this script" | |
echo "3. Ensure a stable internet connection" | |
echo | |
read -p "Press RETURN to continue or CTRL-C to quit…" | |
} | |
############################################################################### | |
# Preferences (abridged) | |
############################################################################### | |
configure_system() { | |
# https://macos-defaults.com | |
log_info "Configuring System Preferences…" | |
defaults write com.apple.dock orientation -string bottom | |
defaults write com.apple.dock autohide -bool true | |
mkdir -p "$HOME/Documents/Screenshots" | |
defaults write com.apple.screencapture location -string "$HOME/Documents/Screenshots" | |
defaults write com.apple.dock "static-only" -bool "true" | |
defaults write com.apple.dock "show-recents" -bool "false" | |
defaults write com.apple.Safari "ShowFullURLInSmartSearchField" -bool "true" | |
defaults write com.apple.universalaccess "showWindowTitlebarIcons" -bool "true" | |
defaults write com.apple.finder "ShowPathbar" -bool "true" | |
defaults write com.apple.menuextra.clock "DateFormat" -string "\"EEE d MMM HH:mm:ss\"" | |
defaults write com.apple.TimeMachine "DoNotOfferNewDisksForBackup" -bool "true" | |
defaults write com.apple.dock "mru-spaces" -bool "false" | |
# https://dev.to/darrinndeal/setting-mac-hot-corners-in-the-terminal-3de | |
defaults write com.apple.dock wvous-bl-corner -int 10 | |
defaults write com.apple.dock wvous-bl-modifier -int 1048576 | |
defaults write com.apple.dock wvous-tl-corner -int 5 | |
defaults write com.apple.dock wvous-tl-modifier -int 1048576 | |
killall Dock Finder Safari SystemUIServer 2>/dev/null || true | |
log_success "System Preferences applied" | |
} | |
############################################################################### | |
# Homebrew – install or update/upgrade | |
############################################################################### | |
brew_bootstrap() { | |
if ! command -v brew &>/dev/null; then | |
log_info "Homebrew not found → installing…" | |
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
else | |
log_info "Homebrew found → updating & upgrading…" | |
brew update; brew upgrade; brew cleanup | |
fi | |
[[ $(uname -m) == arm64 ]] && | |
{ eval "$(/opt/homebrew/bin/brew shellenv)"; | |
grep -q "/opt/homebrew" ~/.zprofile 2>/dev/null || | |
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile; } | |
} | |
############################################################################### | |
# Lists | |
############################################################################### | |
APPS=( | |
# Casks | |
"another-redis-desktop-manager:cask" | |
"appcleaner:cask" | |
"balenaetcher:cask" | |
"discord:cask" | |
"docker:cask" | |
"firefox:cask" | |
"firefox@developer-edition:cask" | |
"github:cask" | |
"chromium:cask:no-quarantine" | |
"handbrake:cask" | |
"iina:cask" | |
"pgadmin4:cask" | |
"rectangle:cask" | |
"tradingview:cask" | |
"transmission:cask" | |
"visual-studio-code:cask" | |
"cursor:cask" | |
"windsurf:cask" | |
"vlc:cask" | |
"zed:cask" | |
"cleanshot:cask" | |
"font-sf-mono:cask" | |
"obs:cask" | |
"sf-symbols:cask" | |
"thunderbird:cask" | |
"opensuperwhisper:cask" | |
"zen:cask" | |
"cleanshot:cask" | |
"screen-studio:cask" | |
# Formulae | |
"wget:formula" | |
"gnupg:formula" | |
"tmux:formula" | |
"mas:formula" | |
"kubernetes-cli:formula" | |
"openshift-cli:formula" | |
"zsh-autosuggestions:formula" | |
"zsh-syntax-highlighting:formula" | |
"zsh-history-substring-search:formula" | |
"powerlevel10k:formula" | |
"macmon:formula" | |
"huggingface-cli:formula" | |
) | |
MAS_APPS=( | |
"1440147259:AdGuard for Safari" | |
"497799835:Xcode" | |
"409183694:Keynote" | |
"408981434:iMovie" | |
"640199958:Developer" | |
"1616822987:Affinity Photo 2" | |
"899247664:TestFlight" | |
"1543920362:Displaperture" | |
"310633997:WhatsApp" | |
"425264550:Blackmagic Disk Speed Test" | |
"409201541:Pages" | |
"747648890:Telegram" | |
"937984704:Amphetamine" | |
"409203825:Numbers" | |
"1475387142:Tailscale" | |
"1433648537:Passepartout" | |
"1579902068:xSearch" | |
"424389933:Final Cut Pro" | |
"434290957:Motion" | |
"424390742:Compressor" | |
) | |
############################################################################### | |
# Portable helper – fill array with command output (no mapfile needed) | |
############################################################################### | |
cmd_to_array() { # $1 = array-name $2 = cmd… | |
local _line | |
eval "$1=()" | |
while IFS= read -r _line; do | |
eval "$1+=(\"\$_line\")" | |
done < <(eval "$2") | |
} | |
############################################################################### | |
# Install Brew items (skip already installed) | |
############################################################################### | |
install_brew_items() { | |
INST_FORMULAE=() | |
INST_CASKS=() | |
cmd_to_array INST_FORMULAE "brew list --formula" | |
cmd_to_array INST_CASKS "brew list --cask 2>/dev/null || true" | |
local total=${#APPS[@]} current=0 kind name pct | |
show_bar 0 "starting…"; newline_below_bar | |
for entry in "${APPS[@]}"; do | |
current=$((current+1)); pct=$(( current * 100 / total )) | |
IFS=':' read -r name kind flag <<< "$entry" | |
if [[ $kind == cask ]] && [[ " ${INST_CASKS[@]-} " == *" $name "* ]] || | |
[[ $kind == formula ]] && [[ " ${INST_FORMULAE[@]-} " == *" $name "* ]]; then | |
show_bar "$pct" "✓ already installed $name"; newline_below_bar; continue | |
fi | |
show_bar "$pct" "↓ $name"; newline_below_bar | |
if [[ $kind == cask ]]; then | |
if [[ $flag == "no-quarantine" ]]; then | |
brew install --cask --no-quarantine "$name" | |
else | |
brew install --cask "$name" | |
fi | |
else | |
brew install "$name" | |
fi | |
show_bar "$pct" "✔︎ $name"; newline_below_bar | |
done | |
brew upgrade; brew cleanup | |
} | |
############################################################################### | |
# Install MAS items – tolerant of the "command not supported" issue | |
############################################################################### | |
install_mas_items() { | |
# make sure mas exists | |
command -v mas >/dev/null || brew install mas | |
# try to capture the list of IDs already on the machine | |
if mas list 1>/tmp/mas_installed 2>/dev/null; then | |
# success → build INST_IDS array from the tmp file | |
INST_IDS=($(awk '{print $1}' /tmp/mas_installed)) | |
else | |
# macOS 14+ (or similar) where mas can't read the account | |
INST_IDS=() # pretend nothing is installed | |
log_warning "Cannot read App Store account status; installs may prompt or fail." | |
fi | |
rm -f /tmp/mas_installed | |
local total=${#MAS_APPS[@]} current=0 id name pct | |
show_bar 0 "starting…"; newline_below_bar | |
for entry in "${MAS_APPS[@]}"; do | |
current=$((current+1)); pct=$(( current * 100 / total )) | |
id=${entry%%:*}; name=${entry#*:} | |
if [[ " ${INST_IDS[@]-} " == *" $id "* ]]; then | |
show_bar "$pct" "✓ already installed $name"; newline_below_bar; continue | |
fi | |
show_bar "$pct" "↓ $name"; newline_below_bar | |
if mas install "$id"; then | |
show_bar "$pct" "✔︎ $name"; newline_below_bar | |
else | |
log_warning "failed: $name" | |
fi | |
done | |
# attempt a bulk upgrade; ignore errors | |
mas upgrade || true | |
} | |
############################################################################### | |
# Z-shell config (same as previous) | |
############################################################################### | |
setup_zsh() { | |
log_info "Configuring Z-shell…" | |
[[ -f ~/.zshrc ]] && mv ~/.zshrc ~/.zshrc.backup.$(date +%Y%m%d_%H%M%S) | |
prefix=$(brew --prefix) | |
cat > ~/.zshrc <<EOF | |
[[ -r "\${XDG_CACHE_HOME:-\$HOME/.cache}/p10k-instant-prompt-\${(%):-%n}.zsh" ]] && | |
source "\${XDG_CACHE_HOME:-\$HOME/.cache}/p10k-instant-prompt-\${(%):-%n}.zsh" | |
source $prefix/share/zsh-autosuggestions/zsh-autosuggestions.zsh | |
source $prefix/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh | |
source $prefix/share/zsh-history-substring-search/zsh-history-substring-search.zsh | |
source $prefix/share/powerlevel10k/powerlevel10k.zsh-theme | |
autoload -U compinit && compinit | |
alias c='clear' rmm='rm -rf' lss='ls -lah' reload='source ~/.zshrc' | |
alias t='tmux' e='code' z='zed' mtop='macmon' | |
[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh | |
EOF | |
[[ $SHELL != "$(which zsh)" ]] && chsh -s "$(which zsh)" | |
log_success "Z-shell configured" | |
} | |
############################################################################### | |
# Main | |
############################################################################### | |
main() { | |
welcome | |
newline_below_bar | |
brew_bootstrap | |
newline_below_bar | |
configure_system | |
newline_below_bar | |
log_info "Installing Homebrew apps…"; newline_below_bar | |
install_brew_items | |
newline_below_bar | |
log_info "Installing Mac App Store apps…"; newline_below_bar | |
install_mas_items | |
newline_below_bar | |
setup_zsh | |
echo -e "\n${GREEN}${BOLD}✨ All done! Consider rebooting.${NC}" | |
} | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment