# Fix RGB Range Limit on MacOS

> Source: <https://gist.github.com/FelikZ/e287c60e0407f9eeb5434471ad050ff9>
> Published: 2025-10-14 17:54:03+00:00

rgb-fix.sh

      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

 
    Show hidden 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

# List displays from plist, matched with system_profiler names by resolution

num=$(python3 -c "

import plistlib, subprocess, json, sys

file = '$TEMP_PLIST'

with open(file, 'rb') as f:

    data = plistlib.load(f)

displays = data['DisplayAnyUserSets']['Configs'][0]['DisplayConfig']

sp_json = json.loads(subprocess.check_output(

    ['system_profiler', 'SPDisplaysDataType', '-json'],

    stderr=subprocess.DEVNULL

))

sp_displays = []

for gpu in sp_json.get('SPDisplaysDataType', []):

    sp_displays.extend(gpu.get('spdisplays_ndrvs', []))

name_by_res = {}

for d in sp_displays:

    pixels = d.get('_spdisplays_pixels', '')

    name = d.get('_name', '')

    if pixels and name:

        name_by_res[pixels.replace(' ', '')] = name

print(len(displays))

print('Connected displays:', file=sys.stderr)

for idx, disp in enumerate(displays):

    info = disp.get('CurrentInfo', disp.get('UnmirrorInfo', {}))

    w = info.get('Wide', 0)

    h = info.get('High', 0)

    scale = info.get('Scale', 1)

    hz = info.get('Hz', 0)

    pw, ph = int(w * scale), int(h * scale)

    name = name_by_res.get(f'{pw}x{ph}', f'Display {idx}')

    print(f'{idx + 1}: {name} ({pw}x{ph} @ {hz}Hz)', file=sys.stderr)

")

if [ -z "$num" ] || [ "$num" -eq 0 ]; then

    error_exit "Unable to parse number of displays from plist."

fi

# 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

verify.sh

      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

 
    Show hidden characters

#!/usr/bin/env bash

set -e

ORIGINAL_PLIST="/Library/Preferences/com.apple.windowserver.displays.plist"

TEMP_DIR="$(mktemp -d)"

TEMP_PLIST="$TEMP_DIR/com.apple.windowserver.displays.plist"

trap 'rm -rf "$TEMP_DIR"' EXIT

sudo cp "$ORIGINAL_PLIST" "$TEMP_PLIST"

sudo chown "$USER" "$TEMP_PLIST"

python3 <<PYEOF

import plistlib, subprocess, json

file = '$TEMP_PLIST'

with open(file, 'rb') as f:

    data = plistlib.load(f)

sp_json = json.loads(subprocess.check_output(

    ['system_profiler', 'SPDisplaysDataType', '-json'],

    stderr=subprocess.DEVNULL

))

sp_displays = []

for gpu in sp_json.get('SPDisplaysDataType', []):

    sp_displays.extend(gpu.get('spdisplays_ndrvs', []))

name_by_res = {}

for d in sp_displays:

    pixels = d.get('_spdisplays_pixels', '')

    name = d.get('_name', '')

    if pixels and name:

        key = pixels.replace(' ', '')

        name_by_res[key] = name

pixel_encoding_map = {0: 'RGB', 1: 'YCbCr 4:2:2', 2: 'YCbCr 4:4:4'}

range_map = {0: 'Limited', 1: 'Full'}

eotf_map = {0: 'SDR', 1: 'HDR'}

for section in ['DisplayAnyUserSets', 'DisplaySets']:

    if section not in data or 'Configs' not in data[section]:

        continue

    configs = data[section]['Configs']

    if not configs:

        continue

    config = configs[0]

    displays = config.get('DisplayConfig', [])

    print(f'\n=== {section} ({len(displays)} display(s)) ===')

    for idx, disp in enumerate(displays):

        uuid = disp.get('UUID', 'N/A')

        link = disp.get('LinkDescription', {})

        info = disp.get('CurrentInfo', disp.get('UnmirrorInfo', {}))

        w = info.get('Wide', 0)

        h = info.get('High', 0)

        scale = info.get('Scale', 1)

        hz = info.get('Hz', 0)

        pixel_w = int(w * scale)

        pixel_h = int(h * scale)

        res_key = f'{pixel_w}x{pixel_h}'

        name = name_by_res.get(res_key, f'Display {idx}')

        print(f'\n  [{idx + 1}] {name} ({pixel_w}x{pixel_h} @ {hz}Hz)')

        print(f'      UUID:          {uuid}')

        if not link:

            print('      LinkDescription: (not set)')

            continue

        bit_depth = link.get('BitDepth', 'N/A')

        pixel_enc = link.get('PixelEncoding', 'N/A')

        rng = link.get('Range', 'N/A')

        eotf = link.get('EOTF', 'N/A')

        pixel_label = pixel_encoding_map.get(pixel_enc, str(pixel_enc))

        range_label = range_map.get(rng, str(rng))

        eotf_label = eotf_map.get(eotf, str(eotf))

        print(f'      BitDepth:      {bit_depth}')

        print(f'      PixelEncoding: {pixel_enc} ({pixel_label})')

        print(f'      Range:         {rng} ({range_label})')

        print(f'      EOTF:          {eotf} ({eotf_label})')

print()

PYEOF
