Last active
December 8, 2025 14:39
-
-
Save FelikZ/e287c60e0407f9eeb5434471ad050ff9 to your computer and use it in GitHub Desktop.
Fix RGB Range Limit on MacOS
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 | |
| # Script to force RGB Color Output on M1 and M2 based Macs for a selected display | |
| # Function to display an error message and exit | |
| function error_exit { | |
| echo "Error: $1" >&2 | |
| exit 1 | |
| } | |
| # Function to check if a command executed successfully | |
| function check_success { | |
| if [ $? -ne 0 ]; then | |
| error_exit "Command failed: $1" | |
| fi | |
| } | |
| set -e | |
| ORIGINAL_PLIST="/Library/Preferences/com.apple.windowserver.displays.plist" | |
| TEMP_DIR="$HOME/Downloads" | |
| TEMP_PLIST="$TEMP_DIR/com.apple.windowserver.displays.plist" | |
| # Step 1: Unlock the file if locked | |
| sudo chflags nouchg "$ORIGINAL_PLIST" || true | |
| echo "Step 1: File unlocked if necessary." | |
| # Step 2: Copy the plist to temp directory | |
| sudo cp "$ORIGINAL_PLIST" "$TEMP_PLIST" | |
| sudo chown "$USER" "$TEMP_PLIST" | |
| check_success "Copying the plist file" | |
| echo "Step 2: File copied successfully." | |
| set +e | |
| # Get number of displays from plist using Python plistlib | |
| num=$(python3 -c " | |
| import plistlib | |
| with open('$TEMP_PLIST', 'rb') as f: | |
| data = plistlib.load(f) | |
| print(len(data['DisplayAnyUserSets']['Configs'][0]['DisplayConfig'])) | |
| " 2>/dev/null) | |
| if [ -z "$num" ] || [ "$num" -eq 0 ]; then | |
| error_exit "Unable to parse number of displays from plist." | |
| fi | |
| # Get display names from system_profiler | |
| mapfile -t display_names < <(system_profiler SPDisplaysDataType | awk '/^ [^:]+:/ { gsub(/:/,""); gsub(/^ /,""); print }' | grep -v '^ ') | |
| # Check if numbers match | |
| if [ ${#display_names[@]} -ne $num ]; then | |
| echo "Mismatch in number of displays detected (${#display_names[@]} vs $num). Using indices only." | |
| display_names=() | |
| for ((j=0; j<num; j++)); do | |
| display_names+=("Display $j") | |
| done | |
| fi | |
| # List displays | |
| echo "Connected displays:" | |
| for j in "${!display_names[@]}"; do | |
| echo "$((j+1)): ${display_names[j]}" | |
| done | |
| # Ask user to choose | |
| read -p "Enter the number of the display to update (1-${num}): " choice | |
| if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "$num" ]; then | |
| error_exit "Invalid choice." | |
| fi | |
| i=$((choice - 1)) | |
| # Get the UUID of the selected display | |
| uuid=$(python3 -c " | |
| import plistlib | |
| with open('$TEMP_PLIST', 'rb') as f: | |
| data = plistlib.load(f) | |
| print(data['DisplayAnyUserSets']['Configs'][0]['DisplayConfig'][$i]['UUID']) | |
| " 2>/dev/null) | |
| if [ -z "$uuid" ]; then | |
| error_exit "Unable to retrieve UUID for the selected display." | |
| fi | |
| echo "Modifying ${uuid}..." | |
| # Ask user for bit depth | |
| read -p "Enter the bit depth (8 or 10): " bit_depth_choice | |
| if ! [[ "$bit_depth_choice" == "8" || "$bit_depth_choice" == "10" ]]; then | |
| error_exit "Invalid bit depth. Please enter 8 or 10." | |
| fi | |
| # Step 4: Modify the plist using embedded Python with plistlib | |
| python3 -c " | |
| import plistlib, sys | |
| file = '$TEMP_PLIST' | |
| uuid = '$uuid' | |
| bit_depth = int('$bit_depth_choice') | |
| link_desc = { | |
| 'BitDepth': bit_depth, | |
| 'EOTF': 0, | |
| 'PixelEncoding': 0, # 0 = RGB | |
| 'Range': 1 # 1 = Full Range | |
| } | |
| with open(file, 'rb') as f: | |
| data = plistlib.load(f) | |
| modified_count = 0 | |
| # Modify DisplayAnyUserSets section | |
| if 'DisplayAnyUserSets' in data and 'Configs' in data['DisplayAnyUserSets']: | |
| for config in data['DisplayAnyUserSets']['Configs']: | |
| if 'DisplayConfig' in config: | |
| for disp in config['DisplayConfig']: | |
| if 'UUID' in disp and disp['UUID'] == uuid: | |
| disp['LinkDescription'] = link_desc.copy() | |
| modified_count += 1 | |
| # Modify DisplaySets section (CRITICAL - was missing before!) | |
| if 'DisplaySets' in data and 'Configs' in data['DisplaySets']: | |
| for config in data['DisplaySets']['Configs']: | |
| if 'DisplayConfig' in config: | |
| for disp in config['DisplayConfig']: | |
| if 'UUID' in disp and disp['UUID'] == uuid: | |
| disp['LinkDescription'] = link_desc.copy() | |
| modified_count += 1 | |
| print(f'Modified {modified_count} display config entries for UUID {uuid}') | |
| with open(file, 'wb') as f: | |
| plistlib.dump(data, f, fmt=plistlib.FMT_BINARY) | |
| " | |
| check_success "Modifying plist with Python" | |
| echo "Step 4: Plist modified successfully." | |
| # Step 5: No conversion needed since edited in binary | |
| # Step 6: Validate the plist | |
| plutil -lint "$TEMP_PLIST" | |
| check_success "Validating plist" | |
| echo "Step 6: Plist validated successfully." | |
| # Step 7: Backup original file (only if backup doesn't exist, to preserve original) | |
| if [ ! -f "${ORIGINAL_PLIST}_backup" ]; then | |
| sudo cp "$ORIGINAL_PLIST" "${ORIGINAL_PLIST}_backup" | |
| check_success "Backing up original file" | |
| echo "Step 7: Original file backed up successfully." | |
| else | |
| echo "Step 7: Backup already exists, skipping (preserving original backup)." | |
| fi | |
| # Step 8: Remove user preferences if exists (they can override system settings) | |
| if [ -f "$HOME/Library/Preferences/com.apple.windowserver.displays.plist" ]; then | |
| if [ ! -f "$HOME/Library/Preferences/com.apple.windowserver.displays.plist_backup" ]; then | |
| mv "$HOME/Library/Preferences/com.apple.windowserver.displays.plist" "$HOME/Library/Preferences/com.apple.windowserver.displays.plist_backup" | |
| check_success "Backing up user preferences" | |
| echo "Step 8: User preferences backed up successfully." | |
| else | |
| rm "$HOME/Library/Preferences/com.apple.windowserver.displays.plist" | |
| echo "Step 8: User preferences removed (backup already exists)." | |
| fi | |
| fi | |
| # Step 9: Remove ByHost preferences if exists (they can override system settings) | |
| for file in "$HOME/Library/Preferences/ByHost/com.apple.windowserver.displays."*.plist; do | |
| if [ -f "$file" ] && [[ "$file" != *_backup ]]; then | |
| if [ ! -f "${file}_backup" ]; then | |
| mv "$file" "${file}_backup" | |
| check_success "Backing up ByHost file" | |
| echo "Step 9: ByHost file backed up successfully." | |
| else | |
| rm "$file" | |
| echo "Step 9: ByHost file removed (backup already exists)." | |
| fi | |
| fi | |
| done | |
| # Step 10: Copy modified plist back | |
| sudo chown "root" "$TEMP_PLIST" | |
| sudo mv "$TEMP_PLIST" "$ORIGINAL_PLIST" | |
| check_success "Copying modified plist back" | |
| echo "Step 10: Modified file copied back successfully." | |
| # Step 11: Set Stationery flag and lock the file (as per original gist) | |
| # Set Stationery Pad flag BEFORE locking (using SetFile if available) | |
| if command -v SetFile &> /dev/null; then | |
| sudo SetFile -a T "$ORIGINAL_PLIST" 2>/dev/null || true | |
| fi | |
| # Lock the file | |
| sudo chflags uchg "$ORIGINAL_PLIST" | |
| check_success "Locking the file" | |
| echo "Step 11: File locked successfully." | |
| # Step 12: Verify the changes were applied | |
| echo "" | |
| echo "=== Verification ===" | |
| link_count=$(python3 -c " | |
| import plistlib | |
| with open('$ORIGINAL_PLIST', 'rb') as f: | |
| data = plistlib.load(f) | |
| count = 0 | |
| for section in ['DisplayAnyUserSets', 'DisplaySets']: | |
| if section in data and 'Configs' in data[section]: | |
| for config in data[section]['Configs']: | |
| for disp in config.get('DisplayConfig', []): | |
| if disp.get('UUID') == '$uuid' and 'LinkDescription' in disp: | |
| count += 1 | |
| print(f'{section}: BitDepth={disp[\"LinkDescription\"].get(\"BitDepth\")}, PixelEncoding={disp[\"LinkDescription\"].get(\"PixelEncoding\")}, Range={disp[\"LinkDescription\"].get(\"Range\")}') | |
| print(f'Total entries modified: {count}') | |
| ") | |
| echo "$link_count" | |
| echo "" | |
| # Step 13: Ask for reboot | |
| read -p "Changes applied. Reboot required. Do you want to reboot now? (Y/N): " reboot_choice | |
| case "$reboot_choice" in | |
| y|Y ) sudo shutdown -r now ;; | |
| n|N ) echo "Reboot canceled. Please reboot manually for changes to take effect." ;; | |
| * ) echo "Invalid choice. Reboot canceled. Please reboot manually." ;; | |
| esac |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To verify that it actually works. (10 Bit, RGB, Full Range)
Method 1
Method 2
via BetterDisplay utility. It has color menu for each monitor, with all possible modes.
PS. The algorithm is based on the following gist