Skip to content

Instantly share code, notes, and snippets.

@trulow
Forked from khronokernel/electron_patcher.py
Last active December 1, 2025 23:04
Show Gist options
  • Select an option

  • Save trulow/d5c69fa07231718bbe4b3a5154dd1f44 to your computer and use it in GitHub Desktop.

Select an option

Save trulow/d5c69fa07231718bbe4b3a5154dd1f44 to your computer and use it in GitHub Desktop.
Electron and Chrome patcher to force OpenGL rendering
"""
electron_patcher.py: Enforce 'use-angle@1' in Chrome and Electron applications
This script patches Chromium-based applications (including Electron apps and Chrome)
to enforce the use of OpenGL rendering through the 'use-angle@1' setting.
Features:
- macOS only: Optimized for macOS Electron applications
- Auto-detection: Automatically finds Electron applications in standard locations
- Specific apps: Has special handling for common apps like Spotify, Discord, VSCode, Microsoft Teams
- Command-line options: Can scan custom directories or patch specific applications
- Deep scanning: Can recursively search for Local State files
- Network mount protection: Avoids scanning network-mounted volumes
Usage examples:
python electron_patcher.py # Patch all detected applications
python electron_patcher.py --list-only # Just list applications without patching
python electron_patcher.py --scan-dir ~/Apps # Scan a custom directory
python electron_patcher.py --app-path ~/path/to/app/Local\ State --app-name "My App"
python electron_patcher.py --deep-scan # Perform deep scan of user's home directory
Version 2.1.1 (2025-03-06)
"""
import enum
import json
import os
import subprocess
# macOS-only script
import platform
if platform.system().lower() != "darwin":
print("Error: This script only works on macOS")
sys.exit(1)
import sys
from pathlib import Path
class ChromiumSettingsPatcher:
class AngleVariant(enum.Enum):
Default = "0"
OpenGL = "1"
Metal = "2"
def __init__(self, state_file: str) -> None:
self._local_state_file = Path(state_file).expanduser()
def patch(self) -> None:
"""
Ensure 'use-angle@1' is set in Chrome's experimental settings
"""
_desired_key = "use-angle"
_desired_value = self.AngleVariant.OpenGL.value
if not self._local_state_file.exists():
print(" Local State missing, creating...")
self._local_state_file.parent.mkdir(parents=True, exist_ok=True)
state_data = {}
else:
print(" Parsing Local State file")
state_data = json.loads(self._local_state_file.read_bytes())
if "browser" not in state_data:
state_data["browser"] = {}
if "enabled_labs_experiments" not in state_data["browser"]:
state_data["browser"]["enabled_labs_experiments"] = []
for key in state_data["browser"]["enabled_labs_experiments"]:
if "@" not in key:
continue
key_pair = key.split("@")
if len(key_pair) < 2:
continue
if key_pair[0] != _desired_key:
continue
if key_pair[1] == _desired_value:
print(f" {_desired_key}@{_desired_value} is already set")
break
index = state_data["browser"]["enabled_labs_experiments"].index(key)
state_data["browser"]["enabled_labs_experiments"][index] = f"{_desired_key}@{_desired_value}"
print(f" Updated {_desired_key}@{_desired_value}")
if f"{_desired_key}@{_desired_value}" not in state_data["browser"]["enabled_labs_experiments"]:
state_data["browser"]["enabled_labs_experiments"].append(f"{_desired_key}@{_desired_value}")
print(f" Added {_desired_key}@{_desired_value}")
print(" Writing to Local State file")
self._local_state_file.write_text(json.dumps(state_data, indent=4))
def is_electron_app(directory):
"""
Check if a directory contains an Electron application
"""
# Check for Local State file (most common indicator)
if (directory / "Local State").exists():
return True
# Check for other common Electron app indicators
electron_indicators = [
"electron.asar",
"app.asar",
"electron-main.js",
"electron.dll",
"Electron Framework.framework"
]
# Look up to 3 levels deep for electron indicators
for level in range(1, 4):
for indicator in electron_indicators:
# Use recursive glob with max depth
matches = list(directory.glob(f"{'*/' * (level-1)}{indicator}"))
if matches:
return True
return False
def get_network_mounts():
"""
Get a list of network mounted volumes on macOS
Returns:
List of paths to mounted network volumes
"""
try:
# Use macOS 'mount' command to list all mounts
result = subprocess.run(['mount'], capture_output=True, text=True)
network_mounts = []
# Look for common network filesystem types
for line in result.stdout.splitlines():
# Common network filesystems on macOS
if any(fs in line for fs in ['nfs', 'smbfs', 'afpfs', 'cifs', 'webdav']):
parts = line.split(' on ')
if len(parts) > 1:
mount_point = parts[1].split(' ')[0]
network_mounts.append(mount_point)
return network_mounts
except Exception as e:
print(f"Warning: Error detecting network mounts: {e}")
return []
def find_local_state_files(start_dir=None):
"""
Walk through directories to find all Local State files on macOS
Args:
start_dir: Directory to start searching from (defaults to user home)
Returns:
List of (path, relative_app_name) tuples for found Local State files
"""
if start_dir is None:
start_dir = os.path.expanduser("~")
else:
start_dir = os.path.expanduser(start_dir)
results = []
print(f"Scanning for Local State files in {start_dir}...")
# Skip these directories to avoid excessive scanning
skip_dirs = [
".Trash",
"node_modules",
"Library/Developer",
"Library/Caches/Homebrew",
"Library/Logs",
"Library/Saved Application State",
]
# Get network mounts to skip
network_mounts = get_network_mounts()
print(f"Detected network mounts that will be skipped: {network_mounts}")
for root, dirs, files in os.walk(start_dir):
# Skip directories that match any in skip_dirs
dirs[:] = [d for d in dirs if not any(skip_path in os.path.join(root, d) for skip_path in skip_dirs)]
# Skip network mounts
dirs[:] = [d for d in dirs if not any(
os.path.join(root, d).startswith(mount) for mount in network_mounts
)]
# Skip hidden directories (start with .)
dirs[:] = [d for d in dirs if not d.startswith('.')]
if "Local State" in files:
path = os.path.join(root, "Local State")
# Get a reasonable app name from the path
relative_path = os.path.relpath(root, start_dir)
parts = relative_path.split(os.sep)
# Use the last non-generic directory name as the app name
app_name = next((p for p in reversed(parts) if p not in ["EBWebView", "User Data", "Default"]), parts[-1])
results.append((path, app_name))
print(f"Found Local State: {path} (app: {app_name})")
return results
def patch_directory(directory_path, name=None, list_only=False):
"""
Patch all Chromium-based applications in the given directory
"""
path = Path(directory_path).expanduser()
if not path.exists():
return
# If it's a specific file path
if not path.is_dir():
if path.name == "Local State":
app_name = name or path.parent.name
if list_only:
print(f"Found {app_name}")
else:
print(f"Patching {app_name}")
patcher = ChromiumSettingsPatcher(path)
patcher.patch()
return
# If it's a directory containing apps
for directory in path.iterdir():
if not directory.is_dir():
continue
# Check for direct Local State file
state_file = directory / "Local State"
if state_file.exists():
if list_only:
print(f"Found {directory.name}")
else:
print(f"Patching {directory.name}")
patcher = ChromiumSettingsPatcher(state_file)
patcher.patch()
continue
# For large directories like Application Support, do deeper scanning
if is_electron_app(directory):
# Find the Local State file
state_files = list(directory.glob("**/Local State"))
for state_file in state_files:
if list_only:
print(f"Found {directory.name} (in {state_file.relative_to(directory).parent})")
else:
print(f"Patching {directory.name} (in {state_file.relative_to(directory).parent})")
patcher = ChromiumSettingsPatcher(state_file)
patcher.patch()
def get_macos_directories():
"""
Return a list of standard macOS directories to scan for Electron applications
"""
return [
"~/Library/Application Support",
"~/Library/Caches",
"~/Library/Preferences",
"~/Library/Containers"
]
def main(list_only=False, deep_scan=False):
# Deep scan option uses find_local_state_files to scan from home directory
if deep_scan:
print("Performing deep scan for Local State files...")
found_files = find_local_state_files()
for state_file_path, app_name in found_files:
if list_only:
print(f"Found {app_name}")
else:
print(f"Patching {app_name}")
patcher = ChromiumSettingsPatcher(state_file_path)
patcher.patch()
return
# Otherwise use the regular directory scanning approach
electron_dirs = get_macos_directories()
# Patch all Electron applications in standard macOS directories
print("Scanning for Electron applications on macOS...")
for directory in electron_dirs:
patch_directory(directory, list_only=list_only)
# Patch Chrome
chrome_path = "~/Library/Application Support/Google"
print("Scanning for Chrome variants...")
patch_directory(chrome_path, list_only=list_only)
# Specific applications with non-standard locations
specific_apps = {
"Spotify": "~/Library/Caches/com.spotify.client/Local State",
"Discord": "~/Library/Application Support/discord/Local State",
"VSCode": "~/Library/Application Support/Code/Local State",
"Slack": "~/Library/Application Support/Slack/Local State",
"Microsoft Teams": "~/Library/Containers/com.microsoft.teams2/Data/Library/Application Support/Microsoft/MSTeams/EBWebView/Local State",
}
print("Checking specific applications...")
for app_name, state_path in specific_apps.items():
patch_directory(state_path, app_name, list_only=list_only)
def parse_arguments():
"""
Parse command-line arguments
"""
import argparse
parser = argparse.ArgumentParser(
description="Patch Electron and Chrome applications to use OpenGL rendering"
)
parser.add_argument(
"--app-path",
help="Path to a specific application's Local State file"
)
parser.add_argument(
"--app-name",
help="Name of the application (used with --app-path)"
)
parser.add_argument(
"--scan-dir",
help="Additional directory to scan for Electron applications"
)
parser.add_argument(
"--list-only",
action="store_true",
help="Only list found applications without patching"
)
parser.add_argument(
"--deep-scan",
action="store_true",
help="Perform a deep scan of the user's home directory for Local State files"
)
return parser.parse_args()
if __name__ == "__main__":
# This script only works on macOS
if platform.system().lower() != "darwin":
print("Error: This script only works on macOS")
sys.exit(1)
args = parse_arguments()
if args.app_path:
# Patch a specific application
app_name = args.app_name or "Custom application"
patch_directory(args.app_path, app_name, list_only=args.list_only)
elif args.scan_dir:
# Scan a specific directory
print(f"Scanning custom directory: {args.scan_dir}")
if args.deep_scan:
found_files = find_local_state_files(args.scan_dir)
for state_file_path, app_name in found_files:
if args.list_only:
print(f"Found {app_name}")
else:
print(f"Patching {app_name}")
patcher = ChromiumSettingsPatcher(state_file_path)
patcher.patch()
else:
patch_directory(args.scan_dir, list_only=args.list_only)
else:
# Run the normal patching process
main(list_only=args.list_only, deep_scan=args.deep_scan)
@cpfc22
Copy link

cpfc22 commented Mar 24, 2025

I'm new this and trying to get my MS teams to work how do I get this to work. I've installed python but not sure what else is next to do.

Any help great needed.

@trulow
Copy link
Author

trulow commented Mar 24, 2025

@cpfc22 Open terminal and navigate to the directory where the script is located.

If the script is on the Desktop, then type

cd ~/Desktop

Change the permissions of the script to make it executable

chmod +x electron_patcher.py

then run by typing

python3 electron_patcher.py

@fablarosa
Copy link

fablarosa commented Apr 14, 2025

I ran the script and Edge, Chrome and Teams are working fine now.

Even running with --deep-scan the script was not able to find anything useful for 1Password and VS Code.

1Password:
~/Library/Application Support/1Password/Local State
NOT found
This folder exists
~/Library/Application Support/1Password/
but I don't know exactly what to look for that could be useful for the fix.

VS Code:
~/Library/Application Support/Code/Local State
NOT found
Don't know if this is relevant, but I have "Settings Sync On" so all VS Code settings are syncronized to my Microsoft account.
I have found this file, looks like it has most VSCode preferences:
~/Library/Application Support/Code/User/globalStorage/storage.json

@trulow
Copy link
Author

trulow commented Apr 14, 2025

@fablarosa Thanks for letting me know. From a quick glance, it seems that VS Code moved the location of the Local State file or it's no longer being used, similar to Discord. I'll have to take a deeper look later today.

@trulow
Copy link
Author

trulow commented Apr 14, 2025

@fablarosa

In the meantime, please try this project. It'll automatically create a separate launcher fixing the OpenGL issue.

https://github.com/trulow/electron_launcher_creator

@fablarosa
Copy link

@trulow thanks a lot for the patcher and launcher scripts, great job!
About VS Code, I noticed that its settings are saved in
~/Library/Application\ Support/Code/User/settings.json
and tried to add this line
"use-angle": "gl",
but it has no effect.
I will rely on the launcher hoping that OCLP will fix this Electron issue in a future release.

@fablarosa
Copy link

I have slightly modified create_vscode_launcher.py so that the launcher app has its own icon (the VS Code icon with the "GL" text).
If you like I can send the new version of the script and the icon file, maybe enabling my account to push on the electron_launcher_creator repo?

@trulow
Copy link
Author

trulow commented Apr 15, 2025

@fablarosa Go ahead and make pull request and I'll review and push the changes.

@fablarosa
Copy link

Sorry but I never worked with pull requests, we just have a small private Gitlab environment.
I tried to create a pull request but the "Create pull request" button is greyed out, should I create a fork of the project first?

@trulow
Copy link
Author

trulow commented Apr 15, 2025

@fablarosa Yeah, you'll have to fork the project first. Implement your updates, then you can push the updates to the main repo and I can then performa a merge.

@pexelva
Copy link

pexelva commented Jul 27, 2025

I am an avid OCLP user and have resurrected many old Macs to run MacOS up to Sequoia 15.5. I have found most of my issue are with my Mac Pro 2013 TrashCan. Since I updated it I started getting the issue with rendering that you script fixes. Unfortunately I am now having a problem with your script not working. I am not a programer but I tinker and can follow directions. I am getting the following error when trying to run your script:

Phillips-Pro:~ pexel$ python3 electron_patcher.py
dyld[8313]: dyld cache '(null)' not loaded: syscall to map cache into shared region failed
dyld[8313]: Library not loaded: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
Referenced from: /Library/Frameworks/Python.framework/Versions/3.6/Resources/Python.app/Contents/MacOS/Python
Reason: tried: '/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file), '/System/Volumes/Preboot/Cryptexes/OS/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file), '/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file, no dyld cache), '/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation' (no such file)
Abort trap: 6

I have upgraded and installed the latest python3 via brew but that has not helped.

Your script fixed all my other apps that were not rendering correctly but I was trying to use Teams as well.

@fablarosa
Copy link

The situation is getting worse, at least on Mac Pro Late 2013 "TrashCan".
Most known web browsers like Chrome and Edge no longer have "use-angle" in their Experiments section, therefore electron_patcher is not a viable solution because this setting is not retained when the browser is relaunched.
I adapted the VS_Code launcher for Edge so that I can launch it with
open "/Applications/Microsoft Edge.app" --args --use-angle=gl
but I'm afraid that, in a not so far future, even this workaround will not work anymore.

@GM-MTEK
Copy link

GM-MTEK commented Dec 1, 2025

I agree, Not sure if anyone else has hit this but my local state patched Teams app just got updated and now is not working. I had to roll back to an earlier release which is working until it gets blocked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment