diff --git a/docs/source/defining-shortcuts.md b/docs/source/defining-shortcuts.md index d40452c8..ce76177e 100644 --- a/docs/source/defining-shortcuts.md +++ b/docs/source/defining-shortcuts.md @@ -69,21 +69,36 @@ Unix systems have the notion of MIME types, while Windows relies more on file na - On Windows, use `file_extensions`. Remember to add the `%1` or `%*` placeholders to your command so the path of the opened file(s) is passed adequately. - On macOS, use `CFBundleDocumentTypes`. Requires no placeholder. The opened document will be - automatically passed as a regular command-line argument. The file will be dispatched via events. - You need to define the `event_handler` field to define a logic that will forward the caught files - to your application (via sockets, API calls, inotify or any other inter-process communication - mechanism) The association happens via UTI strings (Uniform Type Identifiers). If you need UTIs - not defined by Apple, use the `UTImportedTypeDeclarations` field if they are provided by other - apps, or `UTExportedTypeDeclarations` if you are defining them yourself. - -:::{note} -On macOS, this feature uses an additional launcher written in Swift to handle the Apple events. -The Swift runtime libraries are only guaranteed to be available on macOS 10.14.4 and later. -If you need to support older versions of macOS, you will need to instruct your users to install -the Swift runtime libraries manually, available at https://support.apple.com/kb/DL1998. -You can add a dependency on `__osx>=10.14.4` on your conda package if you wish to enforce it. + automatically passed as a regular command-line argument. The association happens via UTI strings + (Uniform Type Identifiers). If you need UTIs not defined by Apple, use the + `UTImportedTypeDeclarations` field if they are provided by other apps, or + `UTExportedTypeDeclarations` if you are defining them yourself. + +(macos-event-handler)= + +:::{admonition} Event handlers in macOS +:class: note + +On macOS, opened files are dispatched via system events. If your application knows how to handle +these events, then you don't need anything else. However, if your app is not aware of system +events, you need to set the `event_handler` field to define a logic that will forward the caught +files to your application (via sockets, API calls, `inotify` or any other inter-process communication +mechanism). [See `event_handler` example](https://github.com/conda/menuinst/blob/e992e76/tests/data/jsons/file_types.json#L35-L57). + +When `event_handler` is set, `menuinst` will inject an additional launcher written in Swift to +handle the Apple events. The Swift runtime libraries are only guaranteed to be available on macOS +10.14.4 and later. If you need to support older versions of macOS, you will need to instruct your +users to install the Swift runtime libraries manually, available at +https://support.apple.com/kb/DL1998. You can add a dependency on `__osx>=10.14.4` on your conda +package if you wish to enforce it. ::: +:::{admonition} Dock blip on macOS +:class: tip + +Note that setting `CFBundleTypeRole` will make the wrapper blip in the dock when the URL is +opened. If you don't want that, do not set it. +::: A multi-platform example: @@ -147,19 +162,8 @@ shortcut. so the URLs are passed adequately. - On Windows, use `url_protocols`. Remember to add the `%1` or `%*` placeholders to your command so the URLs are passed adequately. -- On macOS, use `CFBundleURLTypes`. Requires no placeholders. The URL will be dispatched via - events. You need to define the `event_handler` field to define a logic that will forward the - caught URLs to your application (via sockets, API calls, inotify or any other inter-process - communication mechanism). Note that setting `CFBundleTypeRole` will make the wrapper blip in the - dock when the URL is opened. If you don't want that, do not set it. - -:::{note} -On macOS, this feature uses an additional launcher written in Swift to handle the Apple events. -The Swift runtime libraries are only guaranteed to be available on macOS 10.14.4 and later. -If you need to support older versions of macOS, you will need to instruct your users to install -the Swift runtime libraries manually, available at https://support.apple.com/kb/DL1998. -You can add a dependency on `__osx>=10.14.4` on your conda package if you wish to enforce it. -::: +- On macOS, use `CFBundleURLTypes`. Requires no placeholders. See + {ref}`relevant note in File Types `. ```json { diff --git a/menuinst/platforms/osx.py b/menuinst/platforms/osx.py index f36c7e9b..8639e13b 100644 --- a/menuinst/platforms/osx.py +++ b/menuinst/platforms/osx.py @@ -312,15 +312,15 @@ def _needs_appkit_launcher(self) -> bool: In macOS, file type and URL protocol associations are handled by the Apple Events system. When the user opens on a file or URL, the system will send an Apple Event to the application that was registered as a handler. - We need a special launcher to handle these events and pass them to the - wrapped application in the shortcut. + Some apps might not have the needed listener to process the event. In that case, + we provide a generic one. This is decided by the presence of "event_handler". + If that key is absent or null, we assume the app has its own listener. See: - https://developer.apple.com/library/archive/documentation/Carbon/Conceptual/LaunchServicesConcepts/LSCConcepts/LSCConcepts.html # noqa - The source code at /src/appkit-launcher in this repository """ - needed_keys = ("CFBundleURLTypes", "CFBundleDocumentTypes") - return any([self.metadata.get(k) for k in needed_keys]) + return bool(self.metadata.get("event_handler")) def _lsregister(*args, check=True, **kwargs): diff --git a/news/183-event-handler b/news/183-event-handler new file mode 100644 index 00000000..c2dc30f0 --- /dev/null +++ b/news/183-event-handler @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* Do not inject the AppKit launcher unless `event_handler` has been set. Only affects macOS. (#179 via #183) + +### Deprecations + +* + +### Docs + +* + +### Other + +* diff --git a/tests/data/jsons/file_types_no_event_handler.json b/tests/data/jsons/file_types_no_event_handler.json new file mode 100644 index 00000000..37d63bf4 --- /dev/null +++ b/tests/data/jsons/file_types_no_event_handler.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://schemas.conda.io/menuinst-1.schema.json", + "menu_name": "Example with file type association and no event handler (macOS only)", + "menu_items": [ + { + "name": "FileTypeAssociationNoEventHandler", + "description": "Testing file type association without event handler", + "icon": null, + "command": [ + "{{ PYTHON }}", + "-c", + "import sys, pathlib as p; p.Path(r'__OUTPUT_FILE__').write_text(sys.argv[1])" + ], + "platforms": { + "osx": { + "command": [ + "bash", "-c", "nc -l 40258 > __OUTPUT_FILE__" + ], + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "org.conda.menuinst.filetype-example-no-event-handler", + "CFBundleTypeRole": "Viewer", + "LSItemContentTypes": ["org.conda.menuinst.main-file-util-no-event-handler"], + "LSHandlerRank": "Default" + } + ], + "UTExportedTypeDeclarations": [ + { + "UTTypeConformsTo": ["public.data", "public.content"], + "UTTypeIdentifier": "org.conda.menuinst.main-file-util-no-event-handler", + "UTTypeTagSpecification": { + "public.filename-extension": ["menuinst-no-event-handler"] + } + } + ] + } + } + } + ] +} diff --git a/tests/test_api.py b/tests/test_api.py index 9e43418e..3af88a5a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -43,8 +43,9 @@ def check_output_from_shortcut( file_to_open=None, url_to_open=None, ) -> Tuple[Path, Iterable[Path], str]: - assert action in ("run_shortcut", "open_file", "open_url") + assert action in ("run_shortcut", "open_file", "open_url", None) + output = None output_file = None abs_json_path = DATA / "jsons" / json_path contents = abs_json_path.read_text() @@ -87,7 +88,7 @@ def check_output_from_shortcut( cmd = [str(executable)] process = logged_run(cmd, check=True) output = process.stdout - else: + elif action is not None: if action == "open_file": assert file_to_open is not None with NamedTemporaryFile(suffix=file_to_open, delete=False) as f: @@ -263,6 +264,25 @@ def test_file_type_association(delete_files): assert output.strip().endswith(test_file) +@pytest.mark.skipif(sys.platform != "darwin", reason="Only run on macOS") +@pytest.mark.skipif("CI" not in os.environ, reason="Only run on CI. Export CI=1 to run locally.") +def test_file_type_association_no_event_handler(delete_files, request): + test_file = "test.menuinst-no-event-handler" + abs_json_path, paths, tmp_base_path, _ = check_output_from_shortcut( + delete_files, + "file_types_no_event_handler.json", + action=None, + file_to_open=test_file, + remove_after=False, + ) + request.addfinalizer(lambda: remove(abs_json_path, base_prefix=tmp_base_path)) + app_dir = next(p for p in paths if p.name.endswith(".app")) + info = app_dir / "Contents" / "Info.plist" + plist = plistlib.loads(info.read_bytes()) + cf_bundle_type_name = "org.conda.menuinst.filetype-example-no-event-handler" + assert plist["CFBundleDocumentTypes"][0]["CFBundleTypeName"] == cf_bundle_type_name + + @pytest.mark.skipif("CI" not in os.environ, reason="Only run on CI. Export CI=1 to run locally.") def test_url_protocol_association(delete_files): url = "menuinst://test/"