Skip to content

Pass the opened file or URL to Linux shortcuts via an Exec field code - #536

Open
hmaarrfk wants to merge 3 commits into
conda:mainfrom
hmaarrfk:linux-exec-field-code
Open

Pass the opened file or URL to Linux shortcuts via an Exec field code#536
hmaarrfk wants to merge 3 commits into
conda:mainfrom
hmaarrfk:linux-exec-field-code

Conversation

@hmaarrfk

@hmaarrfk hmaarrfk commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Allow mime types to be auto-infered by filetype association

Claude summary and details

Opened by an AI agent (Claude) acting on behalf of @hmaarrfk, who has reviewed and takes responsibility for this change.

Draft, because the questions under "Open questions" below deserve a maintainer opinion before this is polished.

Description

A Linux menu item that declares MimeType gets registered as a handler for that type (_register_mime_types writes it into mimeapps.list), but the generated Exec line carries no field code. There is no %f, %F, %u or %U anywhere in menuinst/platforms/linux.py, at any version — 2.4.2 has none either, so this is long-standing rather than a regression.

The consequence is that the file manager has nowhere to put the file: it launches the app with no argument.

The type does get registered, so the item shows up in "Open with" and is even installed as the default handler — it just cannot receive what the user opened.

There is a workaround today, and the test fixtures use it: the item author threads a field code through their own command, as tests/data/jsons/file_types.json and url_protocols.json do. That works, but it puts a Linux-specific launcher detail into user metadata, and it is not what menuinst does on the other platforms.

This is what menuinst already does on Windows

WindowsMenuItem._process_command(with_arg1=True) injects %1 into the command, and _register_file_extensions / _register_url_protocols both call it that way:

https://github.com/conda/menuinst/blob/main/menuinst/platforms/win.py#L376-L382

# Ensure "%1" is present and quoted when requested
if with_arg1 and all("%1" not in arg for arg in command):
    command.append('"%1"')

So auto-adding a field code on Linux when the item declares a MimeType is the symmetric behaviour, not a new concept — including the "don't add it if the author already supplied one" guard, which this PR copies.

The bash -c subtlety

LinuxMenuItem._command wraps everything in bash -c '<script>'. A naively appended trailing %f does not work: bash -c SCRIPT NAME ARGS... assigns NAME to $0, so the launcher's argument is swallowed.

$ bash -c 'echo "got:[$*]"' /tmp/file.nc
got:[]
$ bash -c 'echo "got:[$*]"' bash /tmp/file.nc
got:[/tmp/file.nc]

So the emitted form is "$@" inside the quoted script plus a literal bash to occupy $0:

Exec=bash -c '<script> "$@"' bash %f

What the patch does

  • _field_code() returns %u if every declared MIME type is an x-scheme-handler/… scheme, %f if any is a real file type, and "" if there is no MimeType or if the rendered command already contains a field code.
  • _command() takes an optional field_code; when it is empty the output is byte-for-byte what it was before.

Open questions

I picked a default for each of these rather than block on them, and I am happy to change any of them:

  1. %f vs %F. I chose singular %f/%u. They differ only for multi-file selection: %F hands several paths to one process, %f makes the launcher spawn one process per file. Singular is the conservative default for a wrapped bash -c script that may not expect more than one argument, and it mirrors the singular %1 used on Windows. %F/%U would be a one-line change if you prefer it.
  2. %u for URL schemes. x-scheme-handler/… MIME types are how Linux expresses what CFBundleURLTypes expresses on macOS and url_protocols on Windows, and the spec's %u is the URL form, so an item declaring only scheme handlers gets %u. An item declaring both gets %f; that mixed case is arguably better served by %u (which also accepts local paths) — I do not have a strong view.
  3. Inferring from MimeType vs an explicit schema key. Inferring needs no schema change and no action from existing recipes. An explicit key (say platforms.linux.field_code) would be more predictable and would let an author opt out, at the cost of a schema bump. Inference plus the "author already supplied one" escape hatch seemed like the better trade, but this is your call.

Testing

New tests/test_linux_menu_item.py, mirroring the existing tests/test_windows_menu_item.py in structure. It drives _write_desktop_file directly, so it runs on every platform rather than needing a Linux desktop session:

  • no MimeType → no field code
  • file MIME type → %f; x-scheme-handler/… only → %u; mixed → %f
  • a field code already in command → not duplicated, and the "$@" wrapper is not applied
  • an end-to-end check that stands in for the file manager: substitute the field code into Exec, shlex.split it, run it, and assert the command actually received the path. This is the test that fails if the $0 detail above is got wrong.
$ pytest tests/test_linux_menu_item.py     # before the change
4 failed, 2 passed
$ pytest tests/test_linux_menu_item.py     # after
6 passed

(The 2 that pass before are the negative controls, as expected.)

No existing shortcut changes. I rendered the Exec line for every JSON fixture in tests/data/jsons that enables Linux, with and without the patch, and diffed:

file_types.json, url_protocols.json, sys-prefix.json, precommands.json, pwnd.json
→ identical

file_types.json and url_protocols.json are the only fixtures that declare MimeType, and both already carry their own field code, so the guard keeps them untouched. The Linux CI cases for file-type and URL-protocol association should therefore be unaffected.

Full suite on macOS (Python 3.12): 79 passed, 27 skipped, 1 failed. The one failure is tests/test_elevation.py::test_elevation, which fails identically on unmodified main in this environment and is unrelated to this change.

pre-commit run --all-files passes (all 19 hooks).

What I could not test. I have no Linux desktop session here, so I could not run the PLATFORM == "linux" / CI tests — test_file_type_association, test_url_protocol_association, test_desktop_files_escaping — and I could not confirm against a real file manager that a double-click hands the path through as intended. The end-to-end test above emulates the launcher (substitute, split, execute) rather than being one. Linux CI on this PR is the real check.

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation? Not yet — if you are happy with the inference rule I will document it alongside MimeType.

A menu item that declares `MimeType` registers as a handler for that type,
but `Exec` carried no `%f`/`%u` field code, so a file manager had no way to
tell menuinst which file was opened unless the item author threaded a field
code through their own `command`.

Auto-adds a field code when `MimeType` is declared and the command does not
already contain one -- the same thing `WindowsMenuItem._process_command`
does with `%1` when registering file extensions and URL protocols.

`%u` is used when every declared MIME type is an `x-scheme-handler/` scheme,
`%f` otherwise.

`LinuxMenuItem._command` wraps the shortcut in `bash -c '<script>'`, and
`bash -c` assigns its first operand to `$0`. A bare trailing field code
would therefore be silently dropped, so the emitted form is
`bash -c '<script> "$@"' bash %f`, with a literal `bash` occupying `$0`.
@github-project-automation github-project-automation Bot moved this to 🆕 New in 🔎 Review Aug 22, 2026
@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label Aug 22, 2026
@hmaarrfk
hmaarrfk marked this pull request as ready for review August 23, 2026 01:28
@hmaarrfk
hmaarrfk requested a review from a team as a code owner August 23, 2026 01:28
hmaarrfk added a commit to hmaarrfk/menuinst-feedstock that referenced this pull request Aug 23, 2026
<details><summary>Claude's draft</summary>

Refresh this staging branch onto the current feedstock main (menuinst
2.5.2, rattler-build v1 recipe) and carry all four of my open
conda/menuinst pull requests as source patches:

- 333.patch — conda/menuinst#333
  Expand placeholders in lists of strings, not just scalars.
- 334.patch — conda/menuinst#334
  Allow name/command/StartupWMClass/TryExec to be specified per
  target_environment_is_base, and add the linux `run_in_bash` key.
  Rebased onto upstream main: `min_items` -> `min_length` for pydantic
  v2, `Optional[A, B]` -> `Optional[Union[A, B]]`, and SCHEMA_VERSION
  bumped to 1-1-4 with the generated schema/default JSON regenerated so
  `run_in_bash` has a default to resolve against.
- 535.patch — conda/menuinst#535
  Restore the missing f-string prefix on the Icon= desktop entry line.
- 536.patch — conda/menuinst#536
  Pass the opened file or URL to Linux shortcuts via an Exec field code.

334 and 536 both rewrite LinuxMenuItem._command; 536.patch carries the
merged form, which keeps the field-code "$@" trick for the bash-wrapped
case and appends the field code directly when run_in_bash is false.

Build number bumped by 100 and the build restricted to linux-64, since
this is a staging branch for my own channel.

Resume this Claude session:
```
cd /home/mark/git/feedstock/menuinst-feedstock
claude --resume b888d0ac-7cec-4e1e-ac68-9c15e6554011
```
</details>

Claude-Session: https://claude.ai/code/session_0135Eijr6BTzjVHcRcn3JP8w
hmaarrfk added a commit to hmaarrfk/menuinst-feedstock that referenced this pull request Aug 23, 2026
<details><summary>Claude's draft</summary>

Refresh this staging branch onto the current feedstock main (menuinst
2.5.2, rattler-build v1 recipe) and carry all four of my open
conda/menuinst pull requests as source patches:

- 333.patch — conda/menuinst#333
  Expand placeholders in lists of strings, not just scalars.
- 334.patch — conda/menuinst#334
  Allow name/command/StartupWMClass/TryExec to be specified per
  target_environment_is_base, and add the linux `run_in_bash` key.
  Rebased onto upstream main: `min_items` -> `min_length` for pydantic
  v2, `Optional[A, B]` -> `Optional[Union[A, B]]`, and SCHEMA_VERSION
  bumped to 1-1-4 with the generated schema/default JSON regenerated so
  `run_in_bash` has a default to resolve against.
- 535.patch — conda/menuinst#535
  Restore the missing f-string prefix on the Icon= desktop entry line.
- 536.patch — conda/menuinst#536
  Pass the opened file or URL to Linux shortcuts via an Exec field code.

334 and 536 both rewrite LinuxMenuItem._command; 536.patch carries the
merged form, which keeps the field-code "$@" trick for the bash-wrapped
case and appends the field code directly when run_in_bash is false.

Build number bumped by 100 so this can be uploaded to my own channel.
All platforms stay enabled.

Resume this Claude session:
```
cd /home/mark/git/feedstock/menuinst-feedstock
claude --resume b888d0ac-7cec-4e1e-ac68-9c15e6554011
```
</details>

Claude-Session: https://claude.ai/code/session_0135Eijr6BTzjVHcRcn3JP8w
hmaarrfk added a commit to hmaarrfk/menuinst-feedstock that referenced this pull request Aug 23, 2026
<details><summary>Claude's draft</summary>

Rebased onto the noarch-except-windows branch (conda-forge#67) so this staging
build produces one noarch package plus the Windows builds.

Carry all four of my open conda/menuinst pull requests as source
patches:

- 333.patch — conda/menuinst#333
  Expand placeholders in lists of strings, not just scalars.
- 334.patch — conda/menuinst#334
  Allow name/command/StartupWMClass/TryExec to be specified per
  target_environment_is_base, and add the linux `run_in_bash` key.
  Rebased onto upstream main: `min_items` -> `min_length` for pydantic
  v2, `Optional[A, B]` -> `Optional[Union[A, B]]`, and SCHEMA_VERSION
  bumped to 1-1-4 with the generated schema/default JSON regenerated so
  `run_in_bash` has a default to resolve against.
- 535.patch — conda/menuinst#535
  Restore the missing f-string prefix on the Icon= desktop entry line.
- 536.patch — conda/menuinst#536
  Pass the opened file or URL to Linux shortcuts via an Exec field code.

334 and 536 both rewrite LinuxMenuItem._command; 536.patch carries the
merged form, which keeps the field-code "$@" trick for the bash-wrapped
case and appends the field code directly when run_in_bash is false.

Build number bumped by 100 over the base so this can be uploaded to my
own channel.

Resume this Claude session:
```
cd /home/mark/git/feedstock/menuinst-feedstock
claude --resume b888d0ac-7cec-4e1e-ac68-9c15e6554011
```
</details>

Claude-Session: https://claude.ai/code/session_0135Eijr6BTzjVHcRcn3JP8w

@marcoesters marcoesters left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense and achieves parity between Linux and the other platforms, thanks! I just have a few questions/comments.

If you have a few shortcuts to try, we can probably use a virtual machine to see that the code changes are doing what they're supposed to be doing, even though I like your test.

Comment on lines +253 to +254
if all(mime_type.startswith("x-scheme-handler/") for mime_type in mime_types):
return "%u"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with Linux shortcuts. Under which circumstances we have multiple MIME types where one is an x-scheme-handler/ and the other ones aren't? And why is it appropriate to default to %f under these circumstances?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for url-like scehmes like

mailto://

or

sftp://

so you can register something like

conda-forge://

if you so wanted to...

in those cases the "uri" would be valid. otherwise you are declaring your application as needing a "file".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm getting at is that your code requires all MIME types to start with x-scheme-handler/, which suggests there may be multiple MIME types and that they may not all start with that string.

What is a scenario for having multiple MIME types where one is a URI and the other ones aren't, and why is it appropriate to fall back to a file rather than nothing or raising an exception?

Comment thread tests/test_linux_menu_item.py
Comment thread tests/test_linux_menu_item.py Outdated
Comment thread menuinst/platforms/linux.py Outdated
@hmaarrfk

Copy link
Copy Markdown
Contributor Author

Thank you for your thorough review, i will go through this shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed [bot] added once the contributor has signed the CLA

Projects

Status: 🆕 New

Development

Successfully merging this pull request may close these issues.

3 participants