vibefpmgui.compat
Small helpers that differ between macOS, Linux and Windows.
1"""Small helpers that differ between macOS, Linux and Windows.""" 2 3from __future__ import annotations 4 5import os 6import shlex 7import subprocess 8import sys 9 10WINDOWS = os.name == "nt" 11 12 13def split_args(text: str) -> list[str]: 14 """Split a command-line string typed by the user into arguments. 15 16 On Windows backslashes are path separators, not escapes, so POSIX 17 ``shlex.split`` would turn ``C:\\data\\in.txt`` into ``C:datain.txt``; 18 use non-POSIX splitting there and drop the surrounding quotes. 19 """ 20 if not WINDOWS: 21 return shlex.split(text) 22 parts = shlex.split(text, posix=False) 23 return [p[1:-1] if len(p) >= 2 and p[0] == p[-1] and p[0] in "\"'" else p for p in parts] 24 25 26def join_args(args: list[str]) -> str: 27 """A command line to show the user, quoted the way their shell expects.""" 28 return subprocess.list2cmdline(args) if WINDOWS else shlex.join(args) 29 30 31def quote_arg(arg: str) -> str: 32 return subprocess.list2cmdline([arg]) if WINDOWS else shlex.quote(arg) 33 34 35def file_manager_name() -> str: 36 """What the platform's file manager is called (for menu items).""" 37 return {"darwin": "Finder", "win32": "Explorer"}.get(sys.platform, "the file manager") 38 39 40def reveal_label() -> str: 41 return {"darwin": "Show in Finder", "win32": "Show in Explorer"}.get( 42 sys.platform, "Open Containing Folder") 43 44 45def set_windows_app_id(app_id: str = "VibeFPMGUI.VibeFPMGUI"): 46 """On Windows, give the process its own taskbar identity, so the taskbar 47 shows the app's icon instead of grouping it under python.exe.""" 48 if not WINDOWS: 49 return 50 try: 51 import ctypes 52 ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) 53 except (AttributeError, OSError): 54 pass 55 56 57# ------------------------------------------------------------ macOS app name 58# Started as "python -m vibefpmgui" (not from an .app bundle), macOS would call 59# the app "python": in the menu bar, the Dock, and Qt's application menu 60# ("About __main__.py", "Quit __main__.py", from argv[0]). These give it its 61# own name; each does nothing elsewhere or if macOS doesn't allow it. 62def _objc(): 63 """(objc_msgSend caller, class lookup, selector lookup), or None.""" 64 import ctypes 65 import ctypes.util 66 67 objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) 68 ctypes.cdll.LoadLibrary(ctypes.util.find_library("Foundation")) 69 objc.objc_getClass.restype = ctypes.c_void_p 70 objc.sel_registerName.restype = ctypes.c_void_p 71 72 def send(restype, *argtypes): 73 return ctypes.cast(objc.objc_msgSend, ctypes.CFUNCTYPE(restype, ctypes.c_void_p, ctypes.c_void_p, *argtypes)) 74 75 return send, lambda name: objc.objc_getClass(name.encode()), lambda name: objc.sel_registerName(name.encode()) 76 77 78def set_macos_bundle_name(name: str) -> bool: 79 """Before Qt starts: the name in the menu bar (the bundle's CFBundleName, 80 which a python executable has none of). True if it was set.""" 81 if sys.platform != "darwin": 82 return False 83 try: 84 import ctypes 85 86 send, cls, sel = _objc() 87 88 def nsstring(text): 89 return send(ctypes.c_void_p, ctypes.c_char_p)(cls("NSString"), sel("stringWithUTF8String:"), 90 text.encode()) 91 92 bundle = send(ctypes.c_void_p)(cls("NSBundle"), sel("mainBundle")) 93 info = send(ctypes.c_void_p)(bundle, sel("infoDictionary")) 94 if not info: 95 return False 96 send(None, ctypes.c_void_p, ctypes.c_void_p)(info, sel("setObject:forKey:"), nsstring(name), 97 nsstring("CFBundleName")) 98 return True 99 except (OSError, AttributeError, TypeError): 100 return False 101 102 103def set_macos_display_name(name: str) -> bool: 104 """After Qt starts: the name the Dock shows for the running app. A private 105 LaunchServices call (as Chromium uses for its helper processes): if a 106 macOS version lacks it, nothing happens. True if it was set.""" 107 if sys.platform != "darwin": 108 return False 109 try: 110 import ctypes 111 112 services = ctypes.cdll.LoadLibrary( 113 "/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices") 114 core = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation") 115 core.CFStringCreateWithCString.restype = ctypes.c_void_p 116 core.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32] 117 current = services._LSGetCurrentApplicationASN 118 current.restype = ctypes.c_void_p 119 set_item = services._LSSetApplicationInformationItem 120 set_item.restype = ctypes.c_int 121 set_item.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] 122 key = ctypes.c_void_p.in_dll(services, "_kLSDisplayNameKey") 123 value = core.CFStringCreateWithCString(None, name.encode(), 0x08000100) # (UTF-8) 124 asn = current() 125 return bool(asn) and set_item(-2, asn, key, value, None) == 0 126 except (OSError, AttributeError, ValueError, TypeError): 127 return False
14def split_args(text: str) -> list[str]: 15 """Split a command-line string typed by the user into arguments. 16 17 On Windows backslashes are path separators, not escapes, so POSIX 18 ``shlex.split`` would turn ``C:\\data\\in.txt`` into ``C:datain.txt``; 19 use non-POSIX splitting there and drop the surrounding quotes. 20 """ 21 if not WINDOWS: 22 return shlex.split(text) 23 parts = shlex.split(text, posix=False) 24 return [p[1:-1] if len(p) >= 2 and p[0] == p[-1] and p[0] in "\"'" else p for p in parts]
Split a command-line string typed by the user into arguments.
On Windows backslashes are path separators, not escapes, so POSIX
shlex.split would turn C:\data\in.txt into C:datain.txt;
use non-POSIX splitting there and drop the surrounding quotes.
27def join_args(args: list[str]) -> str: 28 """A command line to show the user, quoted the way their shell expects.""" 29 return subprocess.list2cmdline(args) if WINDOWS else shlex.join(args)
A command line to show the user, quoted the way their shell expects.
36def file_manager_name() -> str: 37 """What the platform's file manager is called (for menu items).""" 38 return {"darwin": "Finder", "win32": "Explorer"}.get(sys.platform, "the file manager")
What the platform's file manager is called (for menu items).
46def set_windows_app_id(app_id: str = "VibeFPMGUI.VibeFPMGUI"): 47 """On Windows, give the process its own taskbar identity, so the taskbar 48 shows the app's icon instead of grouping it under python.exe.""" 49 if not WINDOWS: 50 return 51 try: 52 import ctypes 53 ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) 54 except (AttributeError, OSError): 55 pass
On Windows, give the process its own taskbar identity, so the taskbar shows the app's icon instead of grouping it under python.exe.
79def set_macos_bundle_name(name: str) -> bool: 80 """Before Qt starts: the name in the menu bar (the bundle's CFBundleName, 81 which a python executable has none of). True if it was set.""" 82 if sys.platform != "darwin": 83 return False 84 try: 85 import ctypes 86 87 send, cls, sel = _objc() 88 89 def nsstring(text): 90 return send(ctypes.c_void_p, ctypes.c_char_p)(cls("NSString"), sel("stringWithUTF8String:"), 91 text.encode()) 92 93 bundle = send(ctypes.c_void_p)(cls("NSBundle"), sel("mainBundle")) 94 info = send(ctypes.c_void_p)(bundle, sel("infoDictionary")) 95 if not info: 96 return False 97 send(None, ctypes.c_void_p, ctypes.c_void_p)(info, sel("setObject:forKey:"), nsstring(name), 98 nsstring("CFBundleName")) 99 return True 100 except (OSError, AttributeError, TypeError): 101 return False
Before Qt starts: the name in the menu bar (the bundle's CFBundleName, which a python executable has none of). True if it was set.
104def set_macos_display_name(name: str) -> bool: 105 """After Qt starts: the name the Dock shows for the running app. A private 106 LaunchServices call (as Chromium uses for its helper processes): if a 107 macOS version lacks it, nothing happens. True if it was set.""" 108 if sys.platform != "darwin": 109 return False 110 try: 111 import ctypes 112 113 services = ctypes.cdll.LoadLibrary( 114 "/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices") 115 core = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation") 116 core.CFStringCreateWithCString.restype = ctypes.c_void_p 117 core.CFStringCreateWithCString.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32] 118 current = services._LSGetCurrentApplicationASN 119 current.restype = ctypes.c_void_p 120 set_item = services._LSSetApplicationInformationItem 121 set_item.restype = ctypes.c_int 122 set_item.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] 123 key = ctypes.c_void_p.in_dll(services, "_kLSDisplayNameKey") 124 value = core.CFStringCreateWithCString(None, name.encode(), 0x08000100) # (UTF-8) 125 asn = current() 126 return bool(asn) and set_item(-2, asn, key, value, None) == 0 127 except (OSError, AttributeError, ValueError, TypeError): 128 return False
After Qt starts: the name the Dock shows for the running app. A private LaunchServices call (as Chromium uses for its helper processes): if a macOS version lacks it, nothing happens. True if it was set.