Skip to content

Instantly share code, notes, and snippets.

@RafaelMoreira1180778
Created August 7, 2025 10:00
Show Gist options
  • Select an option

  • Save RafaelMoreira1180778/085e540d4b727e3ec95cd49e00b91247 to your computer and use it in GitHub Desktop.

Select an option

Save RafaelMoreira1180778/085e540d4b727e3ec95cd49e00b91247 to your computer and use it in GitHub Desktop.
Update MacOS Icons
# Custom Icon-to-App Mappings
# Format: IconName=App Name Pattern
#
# Supports glob patterns: *, ?, [abc], [a-z]
# Examples:
# Chrome=*Chrome*
# Office=Microsoft [EWP]*
# CustomApp=My Custom App
# Default app mappings (moved from setup_icons.sh)
ActivityMonitor=Activity Monitor
AdobeIllustrator=Adobe Illustrator*
AffinityDesigner=Affinity Designer*
AffinityPhoto=Affinity Photo*
AffinityPublisher=Affinity Publisher*
AfterEffects=Adobe After Effects*
AppleMusic=Music
AppleTV=TV
AppStore=App Store
Chrome=Google Chrome
CreativeCloud=Adobe Creative Cloud
Excel=Microsoft Excel
FinalCut=Final Cut Pro
FindMy=Find My
Firefox=Firefox
Intellij=IntelliJ IDEA*
JetBrainsToolbox=JetBrains Toolbox
Lightroom=Adobe Lightroom*
Photoshop=Adobe Photoshop*
Powerpoint=Microsoft PowerPoint
Premiere=Adobe Premiere Pro*
PyCharm=PyCharm*
Rider=JetBrains Rider
Settings=System Preferences
VoiceMemos=Voice Memos
VSCode=Visual Studio Code
Webstorm=WebStorm
Word=Microsoft Word
Zen=Zen Browser
Amphetamine=Amphetamine
AnkerWork=AnkerWork
Aptakube=Aptakube
Bitwarden=Bitwarden
ChatGPT=ChatGPT
CompanyPortal=Company Portal
Docker=Docker
Falcon=Falcon
Ghostty=Ghostty
GPGKeychain=GPG Keychain
Ice=Ice
iTerm=iTerm
JabraDirect=Jabra Direct
JabraFirmwareUpdate=Jabra Firmware Update
KarabinerElements=Karabiner-Elements
KarabinerEventViewer=Karabiner-EventViewer
KeePassXC=KeePassXC
Keynote=Keynote
KUsrTsk=KUsrTsk
LansweeperAgent=LansweeperAgent
lghub=lghub
logioptionsplus=logioptionsplus
Maccy=Maccy
Magnet=Magnet
MicrosoftEdge=Microsoft Edge
MicrosoftExcel=Microsoft Excel
MicrosoftOneNote=Microsoft OneNote
MicrosoftOutlook=Microsoft Outlook
MicrosoftPowerPoint=Microsoft PowerPoint
MicrosoftTeams=Microsoft Teams
MicrosoftWord=Microsoft Word
NeoHtop=NeoHtop
Numbers=Numbers
OneDrive=OneDrive
Orion=Orion
Pages=Pages
Pearcleaner=Pearcleaner
pgAdmin4=pgAdmin 4
Rectangle=Rectangle
Safari=Safari
Spotify=Spotify
Utilities=Utilities
VisualStudioCode=Visual Studio Code
WhatsApp=WhatsApp
#!/usr/bin/env bash
#
# Custom Icon Setup Script
# Applies custom icons to installed macOS applications using fileicon
#
# Usage: ./setup_icons.sh [options]
# Check Bash version (minimum required: 4)
if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then
echo "This script requires Bash 4 or higher. Your version: $BASH_VERSION" >&2
exit 1
fi
set -euo pipefail
#######################################
# Configuration
#######################################
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly ICONS_DIR="${SCRIPT_DIR}/icons.mac"
readonly CONFIG_FILE="${SCRIPT_DIR}/app_mappings.conf"
readonly APP_DIRS=("/Applications" "${HOME}/Applications" "/System/Applications")
readonly MAX_DEPTH=3
# Counters
SUCCESS_COUNT=0
ERROR_COUNT=0
SKIP_COUNT=0
# Options
VERBOSE=false
QUIET=false
LOG_FILE=""
declare -A APP_MAPPINGS
#######################################
# Utility functions
#######################################
log() {
local level="$1"
shift
local message="$*"
case "$level" in
"ERROR")
[[ "$QUIET" != "true" ]] && echo "❌ $message" >&2
;;
"WARN")
[[ "$QUIET" != "true" ]] && echo "⚠️ $message" >&2
;;
"INFO")
[[ "$QUIET" != "true" ]] && echo "$message"
;;
"VERBOSE")
[[ "$VERBOSE" == "true" ]] && echo "ℹ️ $message"
;;
esac
if [[ -n "$LOG_FILE" ]]; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $level: $message" >>"$LOG_FILE"
fi
}
usage() {
cat <<'EOF'
Custom Icon Setup Script
USAGE:
setup_icons.sh [OPTIONS]
DESCRIPTION:
Applies custom icons to installed macOS applications using fileicon.
Icons are matched by name from the icons/ directory to applications.
OPTIONS:
-v, --verbose Enable verbose output
-q, --quiet Suppress non-error output
-l, --log PATH Save log to specified file
-h, --help Show this help message
EXAMPLES:
./setup_icons.sh # Apply icons with default settings
./setup_icons.sh -v # Verbose output
./setup_icons.sh -l setup.log # Save log to file
CONFIGURATION:
Create app_mappings.conf to customize icon-to-app mappings:
IconName=App Name Pattern
EOF
}
#######################################
# Configuration management
#######################################
load_config() {
# Load mappings from config file
if [[ -f "$CONFIG_FILE" ]]; then
log "VERBOSE" "Loading configuration from $CONFIG_FILE"
while IFS='=' read -r icon_name app_pattern; do
# Skip empty lines and comments
[[ -z "$icon_name" || "$icon_name" =~ ^[[:space:]]*# ]] && continue
# Trim whitespace
icon_name=$(echo "$icon_name" | xargs)
app_pattern=$(echo "$app_pattern" | xargs)
if [[ -n "$icon_name" && -n "$app_pattern" ]]; then
APP_MAPPINGS["$icon_name"]="$app_pattern"
log "VERBOSE" "Loaded mapping: $icon_name -> $app_pattern"
fi
done <"$CONFIG_FILE"
else
log "WARN" "Configuration file not found: $CONFIG_FILE"
log "INFO" "Icons will be matched by filename only"
fi
}
create_sample_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
log "INFO" "Creating sample configuration file: $CONFIG_FILE"
cat >"$CONFIG_FILE" <<'EOF'
# Custom Icon-to-App Mappings
# Format: IconName=App Name Pattern
#
# Supports glob patterns: *, ?, [abc], [a-z]
# Examples:
# Chrome=*Chrome*
# Office=Microsoft [EWP]*
# CustomApp=My Custom App
# Add your custom mappings below:
EOF
else
log "VERBOSE" "Configuration file already exists: $CONFIG_FILE"
fi
}
#######################################
# Dependency management
#######################################
check_dependencies() {
if ! command -v fileicon >/dev/null 2>&1; then
log "ERROR" "fileicon is not installed"
if command -v brew >/dev/null 2>&1; then
log "INFO" "Installing fileicon using Homebrew..."
if brew install fileicon; then
log "INFO" "βœ… fileicon successfully installed"
else
log "ERROR" "Failed to install fileicon"
return 1
fi
else
log "ERROR" "Please install fileicon: brew install fileicon"
log "ERROR" "Or install Homebrew first: https://brew.sh"
return 1
fi
fi
return 0
}
#######################################
# App finding logic
#######################################
contains_glob() {
local string="$1"
[[ "$string" == *"*"* || "$string" == *"?"* || "$string" == *"["* ]]
}
find_apps_with_glob() {
local app_dir="$1"
local pattern="$2"
[[ ! -d "$app_dir" ]] && return 1
# Convert glob pattern to regex for find
local regex_pattern
regex_pattern=$(echo "$pattern" | sed 's/\*/.*$/g' | sed 's/\?/./g')
find "$app_dir" -maxdepth "$MAX_DEPTH" -type d -name "*.app" 2>/dev/null |
while IFS= read -r app_path; do
local app_name
app_name="$(basename "$app_path" .app)"
if [[ "$app_name" =~ ^${regex_pattern} ]]; then
echo "$app_path"
fi
done
}
find_app_exact() {
local app_dir="$1"
local app_name="$2"
[[ ! -d "$app_dir" ]] && return 1
# Try exact match first
local app_path
app_path=$(find "$app_dir" -maxdepth "$MAX_DEPTH" -type d -name "${app_name}.app" -print -quit 2>/dev/null)
[[ -n "$app_path" ]] && echo "$app_path" && return 0
# Try case-insensitive match
app_path=$(find "$app_dir" -maxdepth "$MAX_DEPTH" -type d -iname "${app_name}.app" -print -quit 2>/dev/null)
[[ -n "$app_path" ]] && echo "$app_path" && return 0
return 1
}
find_app_for_icon() {
local icon_name="$1"
local app_name="$icon_name"
# Check for explicit mapping
if [[ -n "${APP_MAPPINGS[$icon_name]:-}" ]]; then
app_name="${APP_MAPPINGS[$icon_name]}"
log "VERBOSE" "Found mapping: '$icon_name' -> '$app_name'"
fi
# Search in all application directories
for app_dir in "${APP_DIRS[@]}"; do
[[ ! -d "$app_dir" ]] && continue
local app_path=""
if contains_glob "$app_name"; then
# Use glob matching
local matches
if matches=$(find_apps_with_glob "$app_dir" "$app_name"); then
app_path=$(echo "$matches" | head -n1)
local match_count
match_count=$(echo "$matches" | wc -l)
if [[ "$match_count" -gt 1 ]]; then
log "VERBOSE" "Multiple matches for pattern '$app_name': $match_count apps, using first"
fi
fi
else
# Use exact matching
app_path=$(find_app_exact "$app_dir" "$app_name")
fi
if [[ -n "$app_path" ]]; then
echo "$app_path"
return 0
fi
done
return 1
}
#######################################
# System app detection
#######################################
is_system_app() {
local app_path="$1"
local app_name
app_name="$(basename "$app_path" .app)"
# Apps in /System/Applications are always system apps
[[ "$app_path" == "/System/Applications/"* ]] && return 0
# Known system apps protected by SIP
local system_apps=(
"Safari"
)
for system_app in "${system_apps[@]}"; do
[[ "$app_name" == "$system_app" ]] && return 0
done
return 1
}
#######################################
# Icon application
#######################################
apply_icon() {
local icon_file="$1"
local app_path="$2"
# Validate inputs
if [[ ! -f "$icon_file" ]]; then
log "ERROR" "Icon file does not exist: $icon_file"
return 1
fi
if [[ ! -d "$app_path" ]]; then
log "ERROR" "Application does not exist: $app_path"
return 1
fi
# Check if this is a system app
if is_system_app "$app_path"; then
local icon_basename app_name
icon_basename="$(basename "$icon_file")"
app_name="$(basename "$app_path" .app)"
log "ERROR" "$icon_basename -> $app_path"
log "VERBOSE" "Cannot modify system app '$app_name' (protected by SIP)"
log "VERBOSE" "To modify system apps, disable SIP in Recovery Mode"
((ERROR_COUNT++))
return 1
fi
# Apply icon with sudo if needed
local exit_code=0
local error_output
if [[ "$app_path" == "/Applications/"* ]]; then
error_output=$(sudo fileicon set "$app_path" "$icon_file" 2>&1) || exit_code=$?
else
error_output=$(fileicon set "$app_path" "$icon_file" 2>&1) || exit_code=$?
fi
local icon_basename
icon_basename="$(basename "$icon_file")"
if [[ "$exit_code" -eq 0 ]]; then
log "INFO" "βœ… $icon_basename -> $app_path"
log "VERBOSE" "Applied successfully"
((SUCCESS_COUNT++))
return 0
else
log "ERROR" "$icon_basename -> $app_path"
log "VERBOSE" "Error: $error_output"
((ERROR_COUNT++))
return 1
fi
}
#######################################
# Main processing
#######################################
process_icons() {
if [[ ! -d "$ICONS_DIR" ]]; then
log "ERROR" "Icons directory does not exist: $ICONS_DIR"
return 1
fi
# Find all .icns files
local icon_files
mapfile -t icon_files < <(find "$ICONS_DIR" -name "*.icns" -type f | sort)
local total_icons=${#icon_files[@]}
if [[ "$total_icons" -eq 0 ]]; then
log "WARN" "No .icns files found in $ICONS_DIR"
return 0
fi
log "INFO" "Processing $total_icons icon files..."
# Check if sudo is needed
local needs_sudo=false
for icon_file in "${icon_files[@]}"; do
local icon_name app_path
icon_name="$(basename "$icon_file" .icns)"
if app_path=$(find_app_for_icon "$icon_name"); then
if [[ "$app_path" == "/Applications/"* ]]; then
needs_sudo=true
break
fi
fi
done
# Request sudo if needed
if [[ "$needs_sudo" == "true" ]]; then
log "INFO" "πŸ” Administrator privileges required for /Applications/"
if ! sudo -v; then
log "ERROR" "Failed to obtain administrator privileges"
return 1
fi
log "VERBOSE" "βœ… Administrator privileges granted"
fi
# Process each icon
local processed=0
for icon_file in "${icon_files[@]}"; do
((processed++))
local icon_basename icon_name
icon_basename="$(basename "$icon_file")"
icon_name="${icon_basename%.icns}"
log "VERBOSE" "Processing $icon_basename ($processed/$total_icons)"
local app_path
if app_path=$(find_app_for_icon "$icon_name"); then
apply_icon "$icon_file" "$app_path"
else
log "INFO" "⏭️ $icon_basename -> App not found"
log "VERBOSE" "Searched: ${APP_DIRS[*]}"
((SKIP_COUNT++))
fi
done
return 0
}
show_summary() {
local total=$((SUCCESS_COUNT + ERROR_COUNT + SKIP_COUNT))
log "INFO" ""
log "INFO" "Summary:"
log "INFO" " βœ… Successfully applied: $SUCCESS_COUNT"
log "INFO" " ❌ Failed: $ERROR_COUNT"
log "INFO" " ⏭️ Skipped (app not found): $SKIP_COUNT"
log "INFO" " πŸ“Š Total processed: $total"
if [[ "$SUCCESS_COUNT" -gt 0 ]]; then
log "INFO" ""
log "INFO" "πŸŽ‰ Icon setup completed! You may need to restart applications to see changes."
fi
[[ -n "$LOG_FILE" ]] && log "INFO" "πŸ“„ Log saved to: $LOG_FILE"
}
#######################################
# Argument parsing
#######################################
parse_arguments() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help)
usage
exit 0
;;
-v | --verbose)
VERBOSE=true
;;
-q | --quiet)
QUIET=true
;;
-l | --log)
if [[ -z "${2:-}" ]]; then
log "ERROR" "Option $1 requires an argument"
return 1
fi
LOG_FILE="$2"
shift
;;
*)
log "ERROR" "Unknown option: $1"
log "ERROR" "Use --help for usage information"
return 1
;;
esac
shift
done
return 0
}
#######################################
# Main function
#######################################
main() {
# Parse arguments
if ! parse_arguments "$@"; then
return 1
fi
# Validate log file
if [[ -n "$LOG_FILE" ]]; then
local log_dir
log_dir="$(dirname "$LOG_FILE")"
if [[ ! -d "$log_dir" ]] || ! touch "$LOG_FILE" 2>/dev/null; then
log "ERROR" "Cannot write to log file: $LOG_FILE"
return 1
fi
fi
log "INFO" "πŸ”§ Custom Icon Setup Script"
log "INFO" "πŸ“ Icons directory: $ICONS_DIR"
log "INFO" "🎯 Target directories: ${APP_DIRS[*]}"
# Check dependencies
if ! check_dependencies; then
return 1
fi
# Load configuration
load_config
create_sample_config
# Process icons
if ! process_icons; then
return 1
fi
log "INFO" "Restarting Dock to apply changes..."
launchctl stop com.apple.Dock.agent
launchctl start com.apple.Dock.agent
log "INFO" "Dock restarted successfully"
log "INFO" "βœ… All tasks completed successfully"
return 0
# Show summary
show_summary
# Return appropriate exit code
[[ "$ERROR_COUNT" -gt 0 ]] && return 1 || return 0
}
# Script entry point
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment