cd /news/developer-tools/fix-rgb-range-limit-on-macos · home topics developer-tools article
[ARTICLE · art-8192] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Fix RGB Range Limit on MacOS

Based on the provided script, this article provides a bash script for macOS that forces RGB color output and full range on M1 and M2 Macs for a selected external display. The script works by copying and modifying the system's display configuration plist file, allowing the user to choose a display and set its bit depth to 8 or 10 bits. It is designed to fix issues where external monitors display limited or incorrect color ranges on Apple Silicon Macs.

read7 min views20 publishedOct 14, 2025

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

function error_exit {

    echo "Error: $1" >&2

    exit 1

}


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"

sudo chflags nouchg "$ORIGINAL_PLIST" || true

echo "Step 1: File unlocked if necessary."

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

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

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))


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}..."


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

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

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


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."

plutil -lint "$TEMP_PLIST"

check_success "Validating plist"

echo "Step 6: Plist validated successfully."

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

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

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

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."

if command -v SetFile &>/dev/null; then

    sudo SetFile -a T "$ORIGINAL_PLIST" 2>/dev/null || true

fi

sudo chflags uchg "$ORIGINAL_PLIST"

check_success "Locking the file"

echo "Step 11: File locked successfully."

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 ""

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

── more in #developer-tools 4 stories · sorted by recency
── more on @macos 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/fix-rgb-range-limit-…] indexed:0 read:7min 2025-10-14 ·