Skip to content

Commit

Permalink
appkit-launcher only injected when event_handler is present (#183)
Browse files Browse the repository at this point in the history
* appkit-launcher only injected when event_handler is present

* add test

* add news and docs

* pre-commit

* Apply suggestions from code review

Co-authored-by: Bianca Henderson <beeankha@gmail.com>

---------

Co-authored-by: Bianca Henderson <beeankha@gmail.com>
  • Loading branch information
jaimergp and beeankha committed Feb 6, 2024
1 parent da5851a commit db52885
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 32 deletions.
56 changes: 30 additions & 26 deletions docs/source/defining-shortcuts.md
Expand Up @@ -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:

Expand Down Expand Up @@ -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 <macos-event-handler>`.

```json
{
Expand Down
8 changes: 4 additions & 4 deletions menuinst/platforms/osx.py
Expand Up @@ -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):
Expand Down
19 changes: 19 additions & 0 deletions news/183-event-handler
@@ -0,0 +1,19 @@
### Enhancements

* <news item>

### Bug fixes

* Do not inject the AppKit launcher unless `event_handler` has been set. Only affects macOS. (#179 via #183)

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>
41 changes: 41 additions & 0 deletions 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"]
}
}
]
}
}
}
]
}
24 changes: 22 additions & 2 deletions tests/test_api.py
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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/"
Expand Down

0 comments on commit db52885

Please sign in to comment.