Last active
April 21, 2026 19:35
-
-
Save gelin/27178bf2a54ba1f416211cef83fcb74a to your computer and use it in GitHub Desktop.
Adds any URL as the Chrome application to the main menu
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
| #!/usr/bin/env python3 | |
| import argparse | |
| import re | |
| import tempfile | |
| import os.path | |
| import subprocess | |
| import urllib.request | |
| import urllib.parse | |
| from PIL import Image | |
| VENDOR = 'chrome' | |
| CHROME_DEFAULT = '/opt/google/chrome/google-chrome' | |
| PROFILE_DEFAULT = 'Default' | |
| def main(): | |
| parser = argparse.ArgumentParser(description='Adds some site as the application to the main menu.') | |
| parser.add_argument('url', help='url of the site to be used as the app') | |
| parser.add_argument('--title', default='Chrome App', help='menu item title (default: Chrome App)') | |
| parser.add_argument('--icon', default=None, help='url to the PNG icon for the app (default: None)') | |
| parser.add_argument('--wmclass', default=None, | |
| help='WM_CLASS for the app (default: based on URL and profile)') | |
| parser.add_argument('--chrome', default=CHROME_DEFAULT, | |
| help=f'path to Chrome binary (default: {CHROME_DEFAULT})') | |
| parser.add_argument('--profile', default=PROFILE_DEFAULT, | |
| help=f'Chrome profile (default: {PROFILE_DEFAULT})') | |
| args = parser.parse_args() | |
| with tempfile.TemporaryDirectory() as tempdir: | |
| wmclass = args.wmclass if args.wmclass else find_class(args.url, args.profile) | |
| icon = download_icon(tempdir, args.icon, wmclass) | |
| icon_name = install_icon(icon, wmclass) | |
| desktop_file = create_desktop_file(tempdir, args.chrome, args.profile, args.url, args.title, icon_name, wmclass) | |
| install_desktop_file(desktop_file) | |
| def find_class(url, profile=PROFILE_DEFAULT): | |
| '''Returns window class from the site URL and Chrome profile. | |
| It has "chrome-" prefix, followed by the URL domain, followed by "__", | |
| followed by the URL path where each "/" is replaced with "_", | |
| followed by "-" and the profile name.''' | |
| parts = urllib.parse.urlsplit(url) | |
| wmclass = VENDOR | |
| wmclass += '-' | |
| wmclass += parts.hostname | |
| wmclass += re.sub(r'^/', '__', parts.path).replace('/', '_') | |
| wmclass += '-' | |
| wmclass += profile.replace(' ', '_') | |
| return wmclass | |
| def download_icon(tempdir, icon_url, wmclass): | |
| # TODO: find icon from site URL | |
| if icon_url is None: | |
| return None | |
| if os.path.exists(icon_url): | |
| print('Using', icon_url, 'as icon') | |
| return icon_url # assuming the icon is a local file | |
| try: | |
| print('Retrieving', icon_url) | |
| filename = os.path.join(tempdir, f'{wmclass}.png') | |
| filename, _ = urllib.request.urlretrieve(icon_url, filename=filename) | |
| return filename | |
| except Exception as e: | |
| print('Failed:', e) | |
| return None | |
| def install_icon(icon_file, wmclass): | |
| if icon_file is None: | |
| return None | |
| try: | |
| icon_name = wmclass | |
| size = resize_icon(icon_file) | |
| print('Installing', icon_file, 'as', icon_name) | |
| subprocess.run( | |
| ['xdg-icon-resource', 'install', '--size', str(size), icon_file, icon_name], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True | |
| ) | |
| return icon_name | |
| except Exception as e: | |
| print('Failed:', e) | |
| return None | |
| def resize_icon(icon_file): | |
| image = Image.open(icon_file) | |
| actual_size, _ = image.size | |
| best_size = find_icon_size(actual_size) | |
| if best_size != actual_size: | |
| print('Resizing', icon_file, 'to', best_size) | |
| resized = image.resize((best_size, best_size), Image.LANCZOS) | |
| resized.save(icon_file) | |
| return best_size | |
| def find_icon_size(actual_size): | |
| if actual_size >= 256: | |
| return 256 | |
| elif actual_size >= 128: | |
| return 128 | |
| elif actual_size >= 64: | |
| return 64 | |
| elif actual_size >= 32: | |
| return 32 | |
| else: | |
| return 16 | |
| def create_desktop_file(tempdir, chrome, profile, url, title, icon_name, wmclass): | |
| desktop_file = os.path.join(tempdir, f'{wmclass}.desktop') | |
| try: | |
| print('Creating', desktop_file) | |
| with open(desktop_file, 'wt') as f: | |
| print('[Desktop Entry]', file=f) | |
| print('Type=Application', file=f) | |
| print(f'Name={title}', file=f) | |
| if icon_name: | |
| print(f'Icon={icon_name}', file=f) | |
| print(f"Exec={chrome} '--profile-directory={profile}' --app={url}", file=f) | |
| print(f'StartupWMClass={wmclass}', file=f) | |
| print('Terminal=false', file=f) | |
| print('Categories=Network', file=f) | |
| return desktop_file | |
| except Exception as e: | |
| print('Failed:', e) | |
| return None | |
| def install_desktop_file(desktop_file): | |
| if desktop_file is None: | |
| return | |
| try: | |
| print('Installing', desktop_file) | |
| subprocess.run( | |
| ['xdg-desktop-menu', 'install', desktop_file], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True | |
| ) | |
| except Exception as e: | |
| print('Failed:', e) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment