Coverage for vibefpmgui/compat.py: 37%

73 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-01 20:51 +0000

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: 20 ↛ 22line 20 didn't jump to line 22 because the condition on line 20 was always true

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: 48 ↛ 50line 48 didn't jump to line 50 because the condition on line 48 was always true

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": 81 ↛ 83line 81 didn't jump to line 83 because the condition on line 81 was always true

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": 107 ↛ 109line 107 didn't jump to line 109 because the condition on line 107 was always true

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