diff --git a/Sage_framework/build_sage_framework.sh b/Sage_framework/build_sage_framework.sh index 19cbf79..257deab 100644 --- a/Sage_framework/build_sage_framework.sh +++ b/Sage_framework/build_sage_framework.sh @@ -87,16 +87,22 @@ ln -s ../../../../../../../share/threejs-sage ${THREEJS_SAGE} mkdir -p ${NBEXTENSIONS}/widgets/notebook ln -s ../../jupyter-js-widgets ${NBEXTENSIONS}/widgets/notebook/js +# Remove useless stuff +rm -rf ${VERSION_DIR}/local/lib/saclib +rm -rf ${VERSION_DIR}/local/share/man + # Fix up rpaths and shebangs echo "Patching files ..." source ../IDs.sh mv files_to_sign files_to_sign.bak -python3 fix_paths.py repo ${VERSION_DIR}/local/bin > files_to_sign +##mv ${VERSION_DIR}/${VENV_DIR}/lib/python${PYTHON_VERSION} ${RESOURCE_DIR} +##python3 fix_paths.py repo ${RESOURCE_DIR}/python${PYTHON_VERSION} > files_to_sign +python3 fix_paths.py repo ${VERSION_DIR}/local/bin >> files_to_sign python3 fix_paths.py repo ${VERSION_DIR}/local/lib >> files_to_sign python3 fix_paths.py repo ${VERSION_DIR}/local/libexec >> files_to_sign python3 fix_paths.py repo ${VERSION_DIR}/${VENV_DIR}/bin >> files_to_sign python3 fix_paths.py repo ${VERSION_DIR}/${VENV_DIR}/lib >> files_to_sign - +##ln -s ../../../../../../Resources/python${PYTHON_VERSION} ${VERSION_DIR}/${VENV_DIR}/lib # Fix the absolute symlinks for the GAP packages pushd ${VERSION_DIR}/local/share/gap/pkg > /dev/null for pkg in `ls` ; do diff --git a/bin/create_dmg b/bin/create_dmg index cb14d7b..aa754d4 100755 --- a/bin/create_dmg +++ b/bin/create_dmg @@ -16,4 +16,5 @@ create-dmg \ --app-drop-link 380 220 \ --icon "Recommended_10_0.pkg" 160 380 \ --format ULFO \ + --no-internet-enable \ $DMG_NAME $SOURCE diff --git a/bin/makedmg b/bin/makedmg deleted file mode 100755 index 77683a6..0000000 --- a/bin/makedmg +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -SAGE_VERSION=`bin/get_sage_version` -SAGE_DASH_VERSION=$(echo $SAGE_VERSION | sed s/\\\./\\\-/g) -SOURCE=SageMath-$SAGE_VERSION -DMG_NAME=SageMath-$SAGE_DASH_VERSION.dmg -VOLUME_NAME=$SOURCE -echo Creating -hdiutil create -volname $VOLUME_NAME -srcfolder $SOURCE temp_$DMG_NAME -echo Compressing -hdiutil convert temp_$DMG_NAME -format ULMO -o $DMG_NAME -rm temp_$DMG_NAME diff --git a/bin/notarize_app b/bin/notarize_app index 9fd26eb..9853afe 100755 --- a/bin/notarize_app +++ b/bin/notarize_app @@ -3,7 +3,7 @@ set -e source IDs.sh SAGE_VERSION=`bin/get_sage_version` SAGE_DASH_VERSION=$(echo $SAGE_VERSION | sed s/\\\./\\\-/g) -DIST=SageMath-$SAGE_VERSION.dmg +DIST=SageMath-$SAGE_VERSION APP=$DIST/SageMath-$SAGE_DASH_VERSION.app DMG=SageMath-$SAGE_VERSION.dmg OPTIONS="--wait --no-progress --apple-id $APPLE_ID \ diff --git a/bin/quick_dmg b/bin/quick_dmg new file mode 100755 index 0000000..2c173b7 --- /dev/null +++ b/bin/quick_dmg @@ -0,0 +1,14 @@ +#!/bin/bash +SAGE_VERSION=`bin/get_sage_version` +SOURCE=SageMath-$SAGE_VERSION +DMG_NAME=SageMath-$SAGE_VERSION.dmg +VOLUME_NAME=$SOURCE +echo Creating ... +hdiutil create -volname $VOLUME_NAME -srcfolder $SOURCE temp_$DMG_NAME +if [ -e $DMG_NAME ]; then + echo Removing old $DMG_NAME + rm -f $DMG_NAME +fi +echo Compressing ... +hdiutil convert temp_$DMG_NAME -format ULFO -o $DMG_NAME +rm temp_$DMG_NAME diff --git a/build_dist.sh b/build_dist.sh index 19f66aa..6712ff8 100644 --- a/build_dist.sh +++ b/build_dist.sh @@ -11,7 +11,7 @@ APP=$DIST/SageMath-$SAGE_DASH_VERSION.app PKG=Recommended_$SAGE_SCORE_VERSION.pkg PYTHON3=../Frameworks/Sage.framework/Versions/Current/venv/bin/python3 mkdir $DIST -# Render templates and nstall the package +# Render templates and install the package cd package . build_package.sh cd .. @@ -30,10 +30,12 @@ cp jinja/output/Info.plist $APP/Contents cp icon/{Sage.icns,sage_icon_1024.png} $APP/Contents/Resources cp logos/{sage_logo_512.png,sage_logo_256.png} $APP/Contents/Resources cp main.py $APP/Contents/Resources +# Build Tcl and Tk frameworks cd TclTk_frameworks make cd .. mv TclTk_frameworks/Frameworks/{Tcl,Tk}.framework $APP/Contents/Frameworks +# Build Sage framework cd Sage_framework bash build_sage_framework.sh cd .. diff --git a/matplotlib_fix/inputhook.py b/matplotlib_fix/inputhook.py new file mode 100644 index 0000000..4a5b720 --- /dev/null +++ b/matplotlib_fix/inputhook.py @@ -0,0 +1,194 @@ +""" +Similar to `PyOS_InputHook` of the Python API, we can plug in an input hook in +the asyncio event loop. + +The way this works is by using a custom 'selector' that runs the other event +loop until the real selector is ready. + +It's the responsibility of this event hook to return when there is input ready. +There are two ways to detect when input is ready: + +The inputhook itself is a callable that receives an `InputHookContext`. This +callable should run the other event loop, and return when the main loop has +stuff to do. There are two ways to detect when to return: + +- Call the `input_is_ready` method periodically. Quit when this returns `True`. + +- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor + becomes readable. (But don't read from it.) + + Note that this is not the same as checking for `sys.stdin.fileno()`. The + eventloop of prompt-toolkit allows thread-based executors, for example for + asynchronous autocompletion. When the completion for instance is ready, we + also want prompt-toolkit to gain control again in order to display that. +""" +import asyncio +import os +import select +import selectors +import threading +from asyncio import AbstractEventLoop +from selectors import BaseSelector, SelectorKey +from typing import ( + TYPE_CHECKING, + Any, + Callable, + List, + Mapping, + NamedTuple, + Optional, + Tuple, +) + +from prompt_toolkit.utils import is_windows + +from .utils import get_event_loop + +__all__ = [ + "new_eventloop_with_inputhook", + "set_eventloop_with_inputhook", + "InputHookSelector", + "InputHookContext", +] + +if TYPE_CHECKING: + from _typeshed import FileDescriptor, FileDescriptorLike + + _EventMask = int + + +def new_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook. + """ + selector = InputHookSelector(selectors.DefaultSelector(), inputhook) + loop = asyncio.SelectorEventLoop(selector) + return loop + +def set_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook, and activate it. + """ + loop = new_eventloop_with_inputhook(inputhook) + asyncio.set_event_loop(loop) + return loop + + +class InputHookSelector(BaseSelector): + """ + Usage: + + selector = selectors.SelectSelector() + loop = asyncio.SelectorEventLoop(InputHookSelector(selector, inputhook)) + asyncio.set_event_loop(loop) + """ + + def __init__( + self, selector: BaseSelector, inputhook: Callable[["InputHookContext"], None] + ) -> None: + self.selector = selector + self.inputhook = inputhook + self._r, self._w = os.pipe() + + def register( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.register(fileobj, events, data=data) + + def unregister(self, fileobj: "FileDescriptorLike") -> "SelectorKey": + return self.selector.unregister(fileobj) + + def modify( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.modify(fileobj, events, data=None) + + def select( + self, timeout: Optional[float] = None + ) -> List[Tuple["SelectorKey", "_EventMask"]]: + # If there are tasks in the current event loop, + # don't run the input hook. + if len(getattr(get_event_loop(), "_ready", [])) > 0: + return self.selector.select(timeout=timeout) + + ready = False + result = None + + # Run selector in other thread. + def run_selector() -> None: + nonlocal ready, result + result = self.selector.select(timeout=timeout) + os.write(self._w, b"x") + ready = True + + th = threading.Thread(target=run_selector) + th.start() + + def input_is_ready() -> bool: + return ready + + # Call inputhook. + # The inputhook function is supposed to return when our selector + # becomes ready. The inputhook can do that by registering the fd in its + # own loop, or by checking the `input_is_ready` function regularly. + +# selector = InputHookSelector(selectors.DefaultSelector(), self.inputhook) + self.inputhook(InputHookContext(self._r, input_is_ready)) + + # Flush the read end of the pipe. + try: + # Before calling 'os.read', call select.select. This is required + # when the gevent monkey patch has been applied. 'os.read' is never + # monkey patched and won't be cooperative, so that would block all + # other select() calls otherwise. + # See: http://www.gevent.org/gevent.os.html + + # Note: On Windows, this is apparently not an issue. + # However, if we would ever want to add a select call, it + # should use `windll.kernel32.WaitForMultipleObjects`, + # because `select.select` can't wait for a pipe on Windows. + if not is_windows(): + select.select([self._r], [], [], None) + + os.read(self._r, 1024) + except OSError: + # This happens when the window resizes and a SIGWINCH was received. + # We get 'Error: [Errno 4] Interrupted system call' + # Just ignore. + pass + + # Wait for the real selector to be done. + th.join() + assert result is not None + return result + + def close(self) -> None: + """ + Clean up resources. + """ + if self._r: + os.close(self._r) + os.close(self._w) + + self._r = self._w = -1 + self.selector.close() + + def get_map(self) -> Mapping["FileDescriptorLike", "SelectorKey"]: + return self.selector.get_map() + + +class InputHookContext: + """ + Given as a parameter to the inputhook. + """ + + def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: + self._fileno = fileno + self.input_is_ready = input_is_ready + + def fileno(self) -> int: + return self._fileno diff --git a/matplotlib_fix/inputhook.py.orig b/matplotlib_fix/inputhook.py.orig new file mode 100644 index 0000000..7490d5b --- /dev/null +++ b/matplotlib_fix/inputhook.py.orig @@ -0,0 +1,193 @@ +""" +Similar to `PyOS_InputHook` of the Python API, we can plug in an input hook in +the asyncio event loop. + +The way this works is by using a custom 'selector' that runs the other event +loop until the real selector is ready. + +It's the responsibility of this event hook to return when there is input ready. +There are two ways to detect when input is ready: + +The inputhook itself is a callable that receives an `InputHookContext`. This +callable should run the other event loop, and return when the main loop has +stuff to do. There are two ways to detect when to return: + +- Call the `input_is_ready` method periodically. Quit when this returns `True`. + +- Add the `fileno` as a watch to the external eventloop. Quit when file descriptor + becomes readable. (But don't read from it.) + + Note that this is not the same as checking for `sys.stdin.fileno()`. The + eventloop of prompt-toolkit allows thread-based executors, for example for + asynchronous autocompletion. When the completion for instance is ready, we + also want prompt-toolkit to gain control again in order to display that. +""" +import asyncio +import os +import select +import selectors +import threading +from asyncio import AbstractEventLoop +from selectors import BaseSelector, SelectorKey +from typing import ( + TYPE_CHECKING, + Any, + Callable, + List, + Mapping, + NamedTuple, + Optional, + Tuple, +) + +from prompt_toolkit.utils import is_windows + +from .utils import get_event_loop + +__all__ = [ + "new_eventloop_with_inputhook", + "set_eventloop_with_inputhook", + "InputHookSelector", + "InputHookContext", +] + +if TYPE_CHECKING: + from _typeshed import FileDescriptor, FileDescriptorLike + + _EventMask = int + + +def new_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook. + """ + selector = InputHookSelector(selectors.DefaultSelector(), inputhook) + loop = asyncio.SelectorEventLoop(selector) + return loop + + +def set_eventloop_with_inputhook( + inputhook: Callable[["InputHookContext"], None] +) -> AbstractEventLoop: + """ + Create a new event loop with the given inputhook, and activate it. + """ + loop = new_eventloop_with_inputhook(inputhook) + asyncio.set_event_loop(loop) + return loop + + +class InputHookSelector(BaseSelector): + """ + Usage: + + selector = selectors.SelectSelector() + loop = asyncio.SelectorEventLoop(InputHookSelector(selector, inputhook)) + asyncio.set_event_loop(loop) + """ + + def __init__( + self, selector: BaseSelector, inputhook: Callable[["InputHookContext"], None] + ) -> None: + self.selector = selector + self.inputhook = inputhook + self._r, self._w = os.pipe() + + def register( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.register(fileobj, events, data=data) + + def unregister(self, fileobj: "FileDescriptorLike") -> "SelectorKey": + return self.selector.unregister(fileobj) + + def modify( + self, fileobj: "FileDescriptorLike", events: "_EventMask", data: Any = None + ) -> "SelectorKey": + return self.selector.modify(fileobj, events, data=None) + + def select( + self, timeout: Optional[float] = None + ) -> List[Tuple["SelectorKey", "_EventMask"]]: + # If there are tasks in the current event loop, + # don't run the input hook. + if len(getattr(get_event_loop(), "_ready", [])) > 0: + return self.selector.select(timeout=timeout) + + ready = False + result = None + + # Run selector in other thread. + def run_selector() -> None: + nonlocal ready, result + result = self.selector.select(timeout=timeout) + os.write(self._w, b"x") + ready = True + + th = threading.Thread(target=run_selector) + th.start() + + def input_is_ready() -> bool: + return ready + + # Call inputhook. + # The inputhook function is supposed to return when our selector + # becomes ready. The inputhook can do that by registering the fd in its + # own loop, or by checking the `input_is_ready` function regularly. + self.inputhook(InputHookContext(self._r, input_is_ready)) + + # Flush the read end of the pipe. + try: + # Before calling 'os.read', call select.select. This is required + # when the gevent monkey patch has been applied. 'os.read' is never + # monkey patched and won't be cooperative, so that would block all + # other select() calls otherwise. + # See: http://www.gevent.org/gevent.os.html + + # Note: On Windows, this is apparently not an issue. + # However, if we would ever want to add a select call, it + # should use `windll.kernel32.WaitForMultipleObjects`, + # because `select.select` can't wait for a pipe on Windows. + if not is_windows(): + select.select([self._r], [], [], None) + + os.read(self._r, 1024) + except OSError: + # This happens when the window resizes and a SIGWINCH was received. + # We get 'Error: [Errno 4] Interrupted system call' + # Just ignore. + pass + + # Wait for the real selector to be done. + th.join() + assert result is not None + return result + + def close(self) -> None: + """ + Clean up resources. + """ + if self._r: + os.close(self._r) + os.close(self._w) + + self._r = self._w = -1 + self.selector.close() + + def get_map(self) -> Mapping["FileDescriptorLike", "SelectorKey"]: + return self.selector.get_map() + + +class InputHookContext: + """ + Given as a parameter to the inputhook. + """ + + def __init__(self, fileno: int, input_is_ready: Callable[[], bool]) -> None: + self._fileno = fileno + self.input_is_ready = input_is_ready + + def fileno(self) -> int: + return self._fileno diff --git a/matplotlib_fix/osx.py b/matplotlib_fix/osx.py new file mode 100644 index 0000000..d4cd561 --- /dev/null +++ b/matplotlib_fix/osx.py @@ -0,0 +1,167 @@ +""" +Calls NSApp / CoreFoundation APIs via ctypes. +""" + +# obj-c boilerplate from appnope, used under BSD 2-clause +import ctypes +import ctypes.util + +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type: ignore + +void_p = ctypes.c_void_p + +objc.objc_getClass.restype = void_p +objc.sel_registerName.restype = void_p +objc.objc_msgSend.restype = void_p +objc.objc_msgSend.argtypes = [void_p, void_p] + +msg = objc.objc_msgSend + +def _utf8(s): + """ensure utf8 bytes""" + if not isinstance(s, bytes): + s = s.encode('utf8') + return s + +def n(name): + """create a selector name (for ObjC methods)""" + return objc.sel_registerName(_utf8(name)) + +def C(classname): + """get an ObjC Class by name""" + return objc.objc_getClass(_utf8(classname)) + +# end obj-c boilerplate from appnope + + +# CoreFoundation C-API calls we will use: +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) # type: ignore + +CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate +CFFileDescriptorCreate.restype = void_p +CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p, void_p] + +CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor +CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int +CFFileDescriptorGetNativeDescriptor.argtypes = [void_p] + +CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks +CFFileDescriptorEnableCallBacks.restype = None +CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong] + +CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource +CFFileDescriptorCreateRunLoopSource.restype = void_p +CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p] + +CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent +CFRunLoopGetCurrent.restype = void_p + +CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource +CFRunLoopAddSource.restype = None +CFRunLoopAddSource.argtypes = [void_p, void_p, void_p] + +CFRelease = CoreFoundation.CFRelease +CFRelease.restype = None +CFRelease.argtypes = [void_p] + +CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate +CFFileDescriptorInvalidate.restype = None +CFFileDescriptorInvalidate.argtypes = [void_p] + +# From CFFileDescriptor.h +kCFFileDescriptorReadCallBack = 1 +kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes') + +def _NSApp(): + """Return the unique NSApplication instance, creating it if necessary.""" + objc.objc_msgSend.argtypes = [void_p, void_p] + return msg(C('NSApplication'), n('sharedApplication')) + +def _wake(NSApp): + """Send an ApplicationDefined event. + + This is needed because NSApplication.stop just sets a flag. The loop does + not stup until an event is processed.""" + objc.objc_msgSend.argtypes = [ + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + ] + event = msg( + C("NSEvent"), + n( + "otherEventWithType:location:modifierFlags:" + "timestamp:windowNumber:context:subtype:data1:data2:" + ), + 15, # Type (NSEventTypeApplicationDefined) + 0, # location + 0, # flags + 0, # timestamp + 0, # window + None, # context + 0, # subtype + 0, # data1 + 0, # data2 + ) + objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p] + msg(NSApp, n('postEvent:atStart:'), void_p(event), True) + +def _input_callback(fdref, flags, info): + """One-shot callback which fires when there is input to be read""" + # Actually this fires every second, no matter what unless the gui + # is stealing all key events. + CFFileDescriptorInvalidate(fdref) + CFRelease(fdref) + objc.objc_msgSend.argtypes = [void_p, void_p, void_p] + NSApp = _NSApp() + # Set the stop flag in the NSApplication + msg(NSApp, n('stop:'), NSApp) + # Send a dummy event to actually stop the runloop. + _wake(NSApp) + +def _stop_on_read(fd): + """Register _input_callback to stop eventloop when fd has data.""" + # Calling the next two lines here instead of at the module level allows + # this module to be loaded in SageMath. + _c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p) + _c_input_callback = _c_callback_func_type(_input_callback) + fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None) + source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0) + loop = CFRunLoopGetCurrent() + CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) + CFRelease(source) + CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack) + +counter = 0 +def inputhook(context): + """Inputhook for Cocoa (NSApp)""" + # This inputhook can be called before the NSApplication is ready to run. + # The hack below waits for a while before actually trying to use it. + global counter + NSApp = _NSApp() + if counter < 50: + return + # This call would register the callback on stdin. + # _stop_on_read(0) + # But instead we register the callback on a pipe in the InputHookContext + # object that we were passed. Using the pipe allows the inputhook + # to be called by other threads, such as an asynchronous autocompleter. + # See prompt_toolkit/eventloop/inputhook.py + _stop_on_read(context.fileno()) + objc.objc_msgSend.argtypes = [void_p, void_p] + msg(NSApp, n('run')) + # A previous version of this eventloop would call + # CoreFoundation.CFRunLoopRun() if the callback had not been run + # since the last call to this inputhook, assuming that meant + # that the last window in the gui had been closed. In fact, this + # input hook gets called every second when the gui has no windows, + # and the callback does not get run in between. The result is + # a hang when %gui osx is run. diff --git a/matplotlib_fix/osx.py.orig b/matplotlib_fix/osx.py.orig new file mode 100644 index 0000000..2754820 --- /dev/null +++ b/matplotlib_fix/osx.py.orig @@ -0,0 +1,157 @@ +"""Inputhook for OS X + +Calls NSApp / CoreFoundation APIs via ctypes. +""" + +# obj-c boilerplate from appnope, used under BSD 2-clause + +import ctypes +import ctypes.util +from threading import Event + +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type: ignore + +void_p = ctypes.c_void_p + +objc.objc_getClass.restype = void_p +objc.sel_registerName.restype = void_p +objc.objc_msgSend.restype = void_p +objc.objc_msgSend.argtypes = [void_p, void_p] + +msg = objc.objc_msgSend + +def _utf8(s): + """ensure utf8 bytes""" + if not isinstance(s, bytes): + s = s.encode('utf8') + return s + +def n(name): + """create a selector name (for ObjC methods)""" + return objc.sel_registerName(_utf8(name)) + +def C(classname): + """get an ObjC Class by name""" + return objc.objc_getClass(_utf8(classname)) + +# end obj-c boilerplate from appnope + +# CoreFoundation C-API calls we will use: +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) # type: ignore + +CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate +CFFileDescriptorCreate.restype = void_p +CFFileDescriptorCreate.argtypes = [void_p, ctypes.c_int, ctypes.c_bool, void_p, void_p] + +CFFileDescriptorGetNativeDescriptor = CoreFoundation.CFFileDescriptorGetNativeDescriptor +CFFileDescriptorGetNativeDescriptor.restype = ctypes.c_int +CFFileDescriptorGetNativeDescriptor.argtypes = [void_p] + +CFFileDescriptorEnableCallBacks = CoreFoundation.CFFileDescriptorEnableCallBacks +CFFileDescriptorEnableCallBacks.restype = None +CFFileDescriptorEnableCallBacks.argtypes = [void_p, ctypes.c_ulong] + +CFFileDescriptorCreateRunLoopSource = CoreFoundation.CFFileDescriptorCreateRunLoopSource +CFFileDescriptorCreateRunLoopSource.restype = void_p +CFFileDescriptorCreateRunLoopSource.argtypes = [void_p, void_p, void_p] + +CFRunLoopGetCurrent = CoreFoundation.CFRunLoopGetCurrent +CFRunLoopGetCurrent.restype = void_p + +CFRunLoopAddSource = CoreFoundation.CFRunLoopAddSource +CFRunLoopAddSource.restype = None +CFRunLoopAddSource.argtypes = [void_p, void_p, void_p] + +CFRelease = CoreFoundation.CFRelease +CFRelease.restype = None +CFRelease.argtypes = [void_p] + +CFFileDescriptorInvalidate = CoreFoundation.CFFileDescriptorInvalidate +CFFileDescriptorInvalidate.restype = None +CFFileDescriptorInvalidate.argtypes = [void_p] + +# From CFFileDescriptor.h +kCFFileDescriptorReadCallBack = 1 +kCFRunLoopCommonModes = void_p.in_dll(CoreFoundation, 'kCFRunLoopCommonModes') + + +def _NSApp(): + """Return the global NSApplication instance (NSApp)""" + objc.objc_msgSend.argtypes = [void_p, void_p] + return msg(C('NSApplication'), n('sharedApplication')) + + +def _wake(NSApp): + """Wake the Application""" + objc.objc_msgSend.argtypes = [ + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + void_p, + ] + event = msg( + C("NSEvent"), + n( + "otherEventWithType:location:modifierFlags:" + "timestamp:windowNumber:context:subtype:data1:data2:" + ), + 15, # Type + 0, # location + 0, # flags + 0, # timestamp + 0, # window + None, # context + 0, # subtype + 0, # data1 + 0, # data2 + ) + objc.objc_msgSend.argtypes = [void_p, void_p, void_p, void_p] + msg(NSApp, n('postEvent:atStart:'), void_p(event), True) + + +_triggered = Event() + +def _input_callback(fdref, flags, info): + """Callback to fire when there's input to be read""" + _triggered.set() + CFFileDescriptorInvalidate(fdref) + CFRelease(fdref) + NSApp = _NSApp() + objc.objc_msgSend.argtypes = [void_p, void_p, void_p] + msg(NSApp, n('stop:'), NSApp) + _wake(NSApp) + +_c_callback_func_type = ctypes.CFUNCTYPE(None, void_p, void_p, void_p) +_c_input_callback = _c_callback_func_type(_input_callback) + + +def _stop_on_read(fd): + """Register callback to stop eventloop when there's data on fd""" + _triggered.clear() + fdref = CFFileDescriptorCreate(None, fd, False, _c_input_callback, None) + CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack) + source = CFFileDescriptorCreateRunLoopSource(None, fdref, 0) + loop = CFRunLoopGetCurrent() + CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes) + CFRelease(source) + + +def inputhook(context): + """Inputhook for Cocoa (NSApp)""" + NSApp = _NSApp() + _stop_on_read(context.fileno()) + objc.objc_msgSend.argtypes = [void_p, void_p] + msg(NSApp, n('run')) + if not _triggered.is_set(): + # app closed without firing callback, + # probably due to last window being closed. + # Run the loop manually in this case, + # since there may be events still to process (#9734) + CoreFoundation.CFRunLoopRun()