1"""Eventloop hook for OS X
2
3Calls NSApp / CoreFoundation APIs via ctypes.
4"""
5
6# cribbed heavily from IPython.terminal.pt_inputhooks.osx
7# obj-c boilerplate from appnope, used under BSD 2-clause
8
9import ctypes
10import ctypes.util
11from threading import Event
12
13objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type:ignore[arg-type]
14
15void_p = ctypes.c_void_p
16
17objc.objc_getClass.restype = void_p
18objc.sel_registerName.restype = void_p
19objc.objc_msgSend.restype = void_p
20
21msg = objc.objc_msgSend
22
23
24def _utf8(s):
25 """ensure utf8 bytes"""
26 if not isinstance(s, bytes):
27 s = s.encode("utf8")
28 return s
29
30
31def n(name):
32 """create a selector name (for ObjC methods)"""
33 return objc.sel_registerName(_utf8(name))
34
35
36def C(classname):
37 """get an ObjC Class by name"""
38 return objc.objc_getClass(_utf8(classname))
39
40
41# end obj-c boilerplate from appnope
42
43# CoreFoundation C-API calls we will use:
44CoreFoundation = ctypes.cdll.LoadLibrary(
45 ctypes.util.find_library("CoreFoundation") # type:ignore[arg-type]
46)
47
48CFAbsoluteTimeGetCurrent = CoreFoundation.CFAbsoluteTimeGetCurrent
49CFAbsoluteTimeGetCurrent.restype = ctypes.c_double
50
51CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent
52CFRunLoopGetCurrent.restype = void_p
53
54CFRunLoopGetMain = CoreFoundation.CFRunLoopGetMain
55CFRunLoopGetMain.restype = void_p
56
57CFRunLoopStop = CoreFoundation.CFRunLoopStop
58CFRunLoopStop.restype = None
59CFRunLoopStop.argtypes = [void_p]
60
61CFRunLoopTimerCreate = CoreFoundation.CFRunLoopTimerCreate
62CFRunLoopTimerCreate.restype = void_p
63CFRunLoopTimerCreate.argtypes = [
64 void_p, # allocator (NULL)
65 ctypes.c_double, # fireDate
66 ctypes.c_double, # interval
67 ctypes.c_int, # flags (0)
68 ctypes.c_int, # order (0)
69 void_p, # callout
70 void_p, # context
71]
72
73CFRunLoopAddTimer = CoreFoundation.CFRunLoopAddTimer
74CFRunLoopAddTimer.restype = None
75CFRunLoopAddTimer.argtypes = [void_p, void_p, void_p]
76
77kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, "kCFRunLoopCommonModes")
78
79
80def _NSApp():
81 """Return the global NSApplication instance (NSApp)"""
82 objc.objc_msgSend.argtypes = [void_p, void_p]
83 return msg(C("NSApplication"), n("sharedApplication"))
84
85
86def _wake(NSApp):
87 """Wake the Application"""
88 objc.objc_msgSend.argtypes = [
89 void_p,
90 void_p,
91 void_p,
92 void_p,
93 void_p,
94 void_p,
95 void_p,
96 void_p,
97 void_p,
98 void_p,
99 void_p,
100 ]
101 event = msg(
102 C("NSEvent"),
103 n(
104 "otherEventWithType:location:modifierFlags:"
105 "timestamp:windowNumber:context:subtype:data1:data2:"
106 ),
107 15, # Type
108 0, # location
109 0, # flags
110 0, # timestamp
111 0, # window
112 None, # context
113 0, # subtype
114 0, # data1
115 0, # data2
116 )
117 objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p]
118 msg(NSApp, n("postEvent:atStart:"), void_p(event), True)
119
120
121_triggered = Event()
122
123
124def stop(timer=None, loop=None):
125 """Callback to fire when there's input to be read"""
126 _triggered.set()
127 NSApp = _NSApp()
128 # if NSApp is not running, stop CFRunLoop directly,
129 # otherwise stop and wake NSApp
130 objc.objc_msgSend.argtypes = [void_p, void_p]
131 if msg(NSApp, n("isRunning")):
132 objc.objc_msgSend.argtypes = [void_p, void_p, void_p]
133 msg(NSApp, n("stop:"), NSApp)
134 _wake(NSApp)
135 else:
136 CFRunLoopStop(CFRunLoopGetCurrent())
137
138
139_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p)
140_c_stop_callback = _c_callback_func_type(stop)
141
142
143def _stop_after(delay):
144 """Register callback to stop eventloop after a delay"""
145 timer = CFRunLoopTimerCreate(
146 None, # allocator
147 CFAbsoluteTimeGetCurrent() + delay, # fireDate
148 0, # interval
149 0, # flags
150 0, # order
151 _c_stop_callback,
152 None,
153 )
154 CFRunLoopAddTimer(
155 CFRunLoopGetMain(),
156 timer,
157 kCFRunLoopCommonModes,
158 )
159
160
161def mainloop(duration=1):
162 """run the Cocoa eventloop for the specified duration (seconds)"""
163
164 _triggered.clear()
165 NSApp = _NSApp()
166 _stop_after(duration)
167 objc.objc_msgSend.argtypes = [void_p, void_p]
168 msg(NSApp, n("run"))
169 if not _triggered.is_set():
170 # app closed without firing callback,
171 # probably due to last window being closed.
172 # Run the loop manually in this case,
173 # since there may be events still to process (ipython/ipython#9734)
174 CoreFoundation.CFRunLoopRun()