Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

appkit-launcher only injected when event_handler is present #183

Merged
merged 5 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 29 additions & 26 deletions docs/source/defining-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,35 @@ 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
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
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
jaimergp marked this conversation as resolved.
Show resolved Hide resolved
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 +161,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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