-
-
Save trulow/d5c69fa07231718bbe4b3a5154dd1f44 to your computer and use it in GitHub Desktop.
""" | |
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 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
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
@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.
In the meantime, please try this project. It'll automatically create a separate launcher fixing the OpenGL issue.
@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.
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?
@fablarosa Go ahead and make pull request and I'll review and push the changes.
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?
@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.
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.