{"slug": "fix-rgb-range-limit-on-macos", "title": "Fix RGB Range Limit on MacOS", "summary": "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.", "body_md": "rgb-fix.sh\n\n      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.\n      \nLearn more about bidirectional Unicode characters\n\n \n    Show hidden characters\n\n#!/usr/bin/env bash\n\n# Script to force RGB Color Output on M1 and M2 based Macs for a selected display\n\n# Function to display an error message and exit\n\nfunction error_exit {\n\n    echo \"Error: $1\" >&2\n\n    exit 1\n\n}\n\n# Function to check if a command executed successfully\n\nfunction check_success {\n\n    if [ $? -ne 0 ]; then\n\n        error_exit \"Command failed: $1\"\n\n    fi\n\n}\n\nset -e\n\nORIGINAL_PLIST=\"/Library/Preferences/com.apple.windowserver.displays.plist\"\n\nTEMP_DIR=\"$HOME/Downloads\"\n\nTEMP_PLIST=\"$TEMP_DIR/com.apple.windowserver.displays.plist\"\n\n# Step 1: Unlock the file if locked\n\nsudo chflags nouchg \"$ORIGINAL_PLIST\" || true\n\necho \"Step 1: File unlocked if necessary.\"\n\n# Step 2: Copy the plist to temp directory\n\nsudo cp \"$ORIGINAL_PLIST\" \"$TEMP_PLIST\"\n\nsudo chown \"$USER\" \"$TEMP_PLIST\"\n\ncheck_success \"Copying the plist file\"\n\necho \"Step 2: File copied successfully.\"\n\nset +e\n\n# List displays from plist, matched with system_profiler names by resolution\n\nnum=$(python3 -c \"\n\nimport plistlib, subprocess, json, sys\n\nfile = '$TEMP_PLIST'\n\nwith open(file, 'rb') as f:\n\n    data = plistlib.load(f)\n\ndisplays = data['DisplayAnyUserSets']['Configs'][0]['DisplayConfig']\n\nsp_json = json.loads(subprocess.check_output(\n\n    ['system_profiler', 'SPDisplaysDataType', '-json'],\n\n    stderr=subprocess.DEVNULL\n\n))\n\nsp_displays = []\n\nfor gpu in sp_json.get('SPDisplaysDataType', []):\n\n    sp_displays.extend(gpu.get('spdisplays_ndrvs', []))\n\nname_by_res = {}\n\nfor d in sp_displays:\n\n    pixels = d.get('_spdisplays_pixels', '')\n\n    name = d.get('_name', '')\n\n    if pixels and name:\n\n        name_by_res[pixels.replace(' ', '')] = name\n\nprint(len(displays))\n\nprint('Connected displays:', file=sys.stderr)\n\nfor idx, disp in enumerate(displays):\n\n    info = disp.get('CurrentInfo', disp.get('UnmirrorInfo', {}))\n\n    w = info.get('Wide', 0)\n\n    h = info.get('High', 0)\n\n    scale = info.get('Scale', 1)\n\n    hz = info.get('Hz', 0)\n\n    pw, ph = int(w * scale), int(h * scale)\n\n    name = name_by_res.get(f'{pw}x{ph}', f'Display {idx}')\n\n    print(f'{idx + 1}: {name} ({pw}x{ph} @ {hz}Hz)', file=sys.stderr)\n\n\")\n\nif [ -z \"$num\" ] || [ \"$num\" -eq 0 ]; then\n\n    error_exit \"Unable to parse number of displays from plist.\"\n\nfi\n\n# Ask user to choose\n\nread -p \"Enter the number of the display to update (1-${num}): \" choice\n\nif ! [[ \"$choice\" =~ ^[0-9]+$ ]] || [ \"$choice\" -lt 1 ] || [ \"$choice\" -gt \"$num\" ]; then\n\n    error_exit \"Invalid choice.\"\n\nfi\n\ni=$((choice - 1))\n\n# Get the UUID of the selected display\n\nuuid=$(python3 -c \"\n\nimport plistlib\n\nwith open('$TEMP_PLIST', 'rb') as f:\n\n    data = plistlib.load(f)\n\n    print(data['DisplayAnyUserSets']['Configs'][0]['DisplayConfig'][$i]['UUID'])\n\n\" 2>/dev/null)\n\nif [ -z \"$uuid\" ]; then\n\n    error_exit \"Unable to retrieve UUID for the selected display.\"\n\nfi\n\necho \"Modifying ${uuid}...\"\n\n# Ask user for bit depth\n\nread -p \"Enter the bit depth (8 or 10): \" bit_depth_choice\n\nif ! [[ \"$bit_depth_choice\" == \"8\" || \"$bit_depth_choice\" == \"10\" ]]; then\n\n    error_exit \"Invalid bit depth. Please enter 8 or 10.\"\n\nfi\n\n# Step 4: Modify the plist using embedded Python with plistlib\n\npython3 -c \"\n\nimport plistlib, sys\n\nfile = '$TEMP_PLIST'\n\nuuid = '$uuid'\n\nbit_depth = int('$bit_depth_choice')\n\nlink_desc = {\n\n    'BitDepth': bit_depth,\n\n    'EOTF': 0,\n\n    'PixelEncoding': 0,  # 0 = RGB\n\n    'Range': 1           # 1 = Full Range\n\n}\n\nwith open(file, 'rb') as f:\n\n    data = plistlib.load(f)\n\nmodified_count = 0\n\n# Modify DisplayAnyUserSets section\n\nif 'DisplayAnyUserSets' in data and 'Configs' in data['DisplayAnyUserSets']:\n\n    for config in data['DisplayAnyUserSets']['Configs']:\n\n        if 'DisplayConfig' in config:\n\n            for disp in config['DisplayConfig']:\n\n                if 'UUID' in disp and disp['UUID'] == uuid:\n\n                    disp['LinkDescription'] = link_desc.copy()\n\n                    modified_count += 1\n\n# Modify DisplaySets section (CRITICAL - was missing before!)\n\nif 'DisplaySets' in data and 'Configs' in data['DisplaySets']:\n\n    for config in data['DisplaySets']['Configs']:\n\n        if 'DisplayConfig' in config:\n\n            for disp in config['DisplayConfig']:\n\n                if 'UUID' in disp and disp['UUID'] == uuid:\n\n                    disp['LinkDescription'] = link_desc.copy()\n\n                    modified_count += 1\n\nprint(f'Modified {modified_count} display config entries for UUID {uuid}')\n\nwith open(file, 'wb') as f:\n\n    plistlib.dump(data, f, fmt=plistlib.FMT_BINARY)\n\n\"\n\ncheck_success \"Modifying plist with Python\"\n\necho \"Step 4: Plist modified successfully.\"\n\n# Step 5: No conversion needed since edited in binary\n\n# Step 6: Validate the plist\n\nplutil -lint \"$TEMP_PLIST\"\n\ncheck_success \"Validating plist\"\n\necho \"Step 6: Plist validated successfully.\"\n\n# Step 7: Backup original file (only if backup doesn't exist, to preserve original)\n\nif [ ! -f \"${ORIGINAL_PLIST}_backup\" ]; then\n\n    sudo cp \"$ORIGINAL_PLIST\" \"${ORIGINAL_PLIST}_backup\"\n\n    check_success \"Backing up original file\"\n\n    echo \"Step 7: Original file backed up successfully.\"\n\nelse\n\n    echo \"Step 7: Backup already exists, skipping (preserving original backup).\"\n\nfi\n\n# Step 8: Remove user preferences if exists (they can override system settings)\n\nif [ -f \"$HOME/Library/Preferences/com.apple.windowserver.displays.plist\" ]; then\n\n    if [ ! -f \"$HOME/Library/Preferences/com.apple.windowserver.displays.plist_backup\" ]; then\n\n        mv \"$HOME/Library/Preferences/com.apple.windowserver.displays.plist\" \"$HOME/Library/Preferences/com.apple.windowserver.displays.plist_backup\"\n\n        check_success \"Backing up user preferences\"\n\n        echo \"Step 8: User preferences backed up successfully.\"\n\n    else\n\n        rm \"$HOME/Library/Preferences/com.apple.windowserver.displays.plist\"\n\n        echo \"Step 8: User preferences removed (backup already exists).\"\n\n    fi\n\nfi\n\n# Step 9: Remove ByHost preferences if exists (they can override system settings)\n\nfor file in \"$HOME/Library/Preferences/ByHost/com.apple.windowserver.displays.\"*.plist; do\n\n    if [ -f \"$file\" ] && [[ \"$file\" != *_backup ]]; then\n\n        if [ ! -f \"${file}_backup\" ]; then\n\n            mv \"$file\" \"${file}_backup\"\n\n            check_success \"Backing up ByHost file\"\n\n            echo \"Step 9: ByHost file backed up successfully.\"\n\n        else\n\n            rm \"$file\"\n\n            echo \"Step 9: ByHost file removed (backup already exists).\"\n\n        fi\n\n    fi\n\ndone\n\n# Step 10: Copy modified plist back\n\nsudo chown \"root\" \"$TEMP_PLIST\"\n\nsudo mv \"$TEMP_PLIST\" \"$ORIGINAL_PLIST\"\n\ncheck_success \"Copying modified plist back\"\n\necho \"Step 10: Modified file copied back successfully.\"\n\n# Step 11: Set Stationery flag and lock the file (as per original gist)\n\n# Set Stationery Pad flag BEFORE locking (using SetFile if available)\n\nif command -v SetFile &>/dev/null; then\n\n    sudo SetFile -a T \"$ORIGINAL_PLIST\" 2>/dev/null || true\n\nfi\n\n# Lock the file\n\nsudo chflags uchg \"$ORIGINAL_PLIST\"\n\ncheck_success \"Locking the file\"\n\necho \"Step 11: File locked successfully.\"\n\n# Step 12: Verify the changes were applied\n\necho \"\"\n\necho \"=== Verification ===\"\n\nlink_count=$(python3 -c \"\n\nimport plistlib\n\nwith open('$ORIGINAL_PLIST', 'rb') as f:\n\n    data = plistlib.load(f)\n\ncount = 0\n\nfor section in ['DisplayAnyUserSets', 'DisplaySets']:\n\n    if section in data and 'Configs' in data[section]:\n\n        for config in data[section]['Configs']:\n\n            for disp in config.get('DisplayConfig', []):\n\n                if disp.get('UUID') == '$uuid' and 'LinkDescription' in disp:\n\n                    count += 1\n\n                    print(f'{section}: BitDepth={disp[\\\"LinkDescription\\\"].get(\\\"BitDepth\\\")}, PixelEncoding={disp[\\\"LinkDescription\\\"].get(\\\"PixelEncoding\\\")}, Range={disp[\\\"LinkDescription\\\"].get(\\\"Range\\\")}')\n\nprint(f'Total entries modified: {count}')\n\n\")\n\necho \"$link_count\"\n\necho \"\"\n\n# Step 13: Ask for reboot\n\nread -p \"Changes applied. Reboot required. Do you want to reboot now? (Y/N): \" reboot_choice\n\ncase \"$reboot_choice\" in\n\ny | Y) sudo shutdown -r now ;;\n\nn | N) echo \"Reboot canceled. Please reboot manually for changes to take effect.\" ;;\n\n*) echo \"Invalid choice. Reboot canceled. Please reboot manually.\" ;;\n\nesac\n\nverify.sh\n\n      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.\n      \nLearn more about bidirectional Unicode characters\n\n \n    Show hidden characters\n\n#!/usr/bin/env bash\n\nset -e\n\nORIGINAL_PLIST=\"/Library/Preferences/com.apple.windowserver.displays.plist\"\n\nTEMP_DIR=\"$(mktemp -d)\"\n\nTEMP_PLIST=\"$TEMP_DIR/com.apple.windowserver.displays.plist\"\n\ntrap 'rm -rf \"$TEMP_DIR\"' EXIT\n\nsudo cp \"$ORIGINAL_PLIST\" \"$TEMP_PLIST\"\n\nsudo chown \"$USER\" \"$TEMP_PLIST\"\n\npython3 <<PYEOF\n\nimport plistlib, subprocess, json\n\nfile = '$TEMP_PLIST'\n\nwith open(file, 'rb') as f:\n\n    data = plistlib.load(f)\n\nsp_json = json.loads(subprocess.check_output(\n\n    ['system_profiler', 'SPDisplaysDataType', '-json'],\n\n    stderr=subprocess.DEVNULL\n\n))\n\nsp_displays = []\n\nfor gpu in sp_json.get('SPDisplaysDataType', []):\n\n    sp_displays.extend(gpu.get('spdisplays_ndrvs', []))\n\nname_by_res = {}\n\nfor d in sp_displays:\n\n    pixels = d.get('_spdisplays_pixels', '')\n\n    name = d.get('_name', '')\n\n    if pixels and name:\n\n        key = pixels.replace(' ', '')\n\n        name_by_res[key] = name\n\npixel_encoding_map = {0: 'RGB', 1: 'YCbCr 4:2:2', 2: 'YCbCr 4:4:4'}\n\nrange_map = {0: 'Limited', 1: 'Full'}\n\neotf_map = {0: 'SDR', 1: 'HDR'}\n\nfor section in ['DisplayAnyUserSets', 'DisplaySets']:\n\n    if section not in data or 'Configs' not in data[section]:\n\n        continue\n\n    configs = data[section]['Configs']\n\n    if not configs:\n\n        continue\n\n    config = configs[0]\n\n    displays = config.get('DisplayConfig', [])\n\n    print(f'\\n=== {section} ({len(displays)} display(s)) ===')\n\n    for idx, disp in enumerate(displays):\n\n        uuid = disp.get('UUID', 'N/A')\n\n        link = disp.get('LinkDescription', {})\n\n        info = disp.get('CurrentInfo', disp.get('UnmirrorInfo', {}))\n\n        w = info.get('Wide', 0)\n\n        h = info.get('High', 0)\n\n        scale = info.get('Scale', 1)\n\n        hz = info.get('Hz', 0)\n\n        pixel_w = int(w * scale)\n\n        pixel_h = int(h * scale)\n\n        res_key = f'{pixel_w}x{pixel_h}'\n\n        name = name_by_res.get(res_key, f'Display {idx}')\n\n        print(f'\\n  [{idx + 1}] {name} ({pixel_w}x{pixel_h} @ {hz}Hz)')\n\n        print(f'      UUID:          {uuid}')\n\n        if not link:\n\n            print('      LinkDescription: (not set)')\n\n            continue\n\n        bit_depth = link.get('BitDepth', 'N/A')\n\n        pixel_enc = link.get('PixelEncoding', 'N/A')\n\n        rng = link.get('Range', 'N/A')\n\n        eotf = link.get('EOTF', 'N/A')\n\n        pixel_label = pixel_encoding_map.get(pixel_enc, str(pixel_enc))\n\n        range_label = range_map.get(rng, str(rng))\n\n        eotf_label = eotf_map.get(eotf, str(eotf))\n\n        print(f'      BitDepth:      {bit_depth}')\n\n        print(f'      PixelEncoding: {pixel_enc} ({pixel_label})')\n\n        print(f'      Range:         {rng} ({range_label})')\n\n        print(f'      EOTF:          {eotf} ({eotf_label})')\n\nprint()\n\nPYEOF", "url": "https://wpnews.pro/news/fix-rgb-range-limit-on-macos", "canonical_source": "https://gist.github.com/FelikZ/e287c60e0407f9eeb5434471ad050ff9", "published_at": "2025-10-14 17:54:03+00:00", "updated_at": "2026-05-22 09:00:18.409499+00:00", "lang": "en", "topics": ["developer-tools", "open-source"], "entities": ["MacOS", "M1", "M2", "Apple"], "alternates": {"html": "https://wpnews.pro/news/fix-rgb-range-limit-on-macos", "markdown": "https://wpnews.pro/news/fix-rgb-range-limit-on-macos.md", "text": "https://wpnews.pro/news/fix-rgb-range-limit-on-macos.txt", "jsonld": "https://wpnews.pro/news/fix-rgb-range-limit-on-macos.jsonld"}}