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
WINDOWS = False
def split_args(text: str) -> list[str]:
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.

def join_args(args: list[str]) -> str:
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.

def quote_arg(arg: str) -> str:
32def quote_arg(arg: str) -> str:
33    return subprocess.list2cmdline([arg]) if WINDOWS else shlex.quote(arg)
def file_manager_name() -> str:
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).

def reveal_label() -> str:
41def reveal_label() -> str:
42    return {"darwin": "Show in Finder", "win32": "Show in Explorer"}.get(
43        sys.platform, "Open Containing Folder")
def set_windows_app_id(app_id: str = 'VibeFPMGUI.VibeFPMGUI'):
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.

def set_macos_bundle_name(name: str) -> bool:
 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.

def set_macos_display_name(name: str) -> bool:
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.