Skip to content

feat(desktop): open a local file with a chosen application - #32

Merged
ralyodio merged 1 commit into
mainfrom
feat/open-with
Sep 6, 2026
Merged

feat(desktop): open a local file with a chosen application#32
ralyodio merged 1 commit into
mainfrom
feat/open-with

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Right-clicking a local file now offers Open with…. The dialog opens with the system default preselected and the applications registered for that file type listed under it, so the ordinary case is just Enter and picking a different one is there when you want it.

How it finds them

From gio, which is what the desktop itself consults: gio info for the content type, gio mime TYPE for the default and the registered set, and the entry's own Exec line via gio launch rather than reconstructing its argument syntax. With no handler chosen it is shell.openPath — the plain double-click.

Two parsing details that are wrong in the obvious implementation

Both covered by tests against real /usr/share/applications entries:

  • Localised keys. Name[zh_TW] comes after Name in the file, so matching Name anywhere names btop "系統監視器".
  • Action groups. A .desktop file can carry [Desktop Action new-window] with its own Name=. Only the [Desktop Entry] group is read.

NoDisplay and Hidden entries are skipped — that is plumbing the desktop keeps out of menus, and this is a menu. A Terminal=true application says "Runs in a terminal", because launching one from a file manager usually flashes and exits, and a choice that appears to do nothing is worse than one that is labelled.

Scope and safety

The item is disabled for a pane pointed at a server (that path names a file on that machine, so there is nothing here to hand to a local application) and for directories. handlerId is validated as a bare *.desktop filename in the main process and resolved only against the XDG application directories — a renderer that could pass a path there would be choosing which program runs.

One more instance of a trap from earlier today

The list is a bounded flex column with the ScrollArea as min-h-0 flex-1, not max-h on the ScrollArea root. The root form does not clip: the viewport is size-full, so with no definite height it grows to fit and the last rows are cut off by the dialog with no way to scroll to them. Same trap as the mirror preview's delete list, found the same way — by looking at it.

Verification

Headless Chromium under the app's real CSP, both themes: the menu item appears on a local file and is disabled on a directory, the dialog lists the handlers with the default marked, Open with nothing chosen sends a null handler, and choosing Visual Studio Code sends code.desktop.

pnpm test 546 passing, pnpm -r typecheck clean, pnpm smoke:desktop all three guards ok.

What is not verified: handler enumeration end to end. This box has gio but no registered desktop associations, so gio mime returns nothing for every type. The parsers are tested against real gio output and real desktop files; the live lookup on a machine with actual applications installed is not.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52

Right-clicking a local file now offers "Open with…". It opens a dialog with
the system default preselected and the applications registered for that file
type listed under it, so the ordinary case is Enter and picking a different
one is there when you want it.

The handlers come from `gio`, which is what the desktop itself consults:
`gio info` for the content type, `gio mime TYPE` for the default and the
registered set, and the entry's own Exec line via `gio launch` rather than us
reconstructing its argument syntax. With no handler chosen it is
`shell.openPath`, the plain double-click.

Two details in the parsing that are wrong in the obvious implementation, both
covered by tests against real /usr/share/applications entries:

- A .desktop file carries localised keys. `Name[zh_TW]` comes after `Name`, so
  matching `Name` anywhere in the file names btop "系統監視器".
- It also carries action groups (`[Desktop Action new-window]`) with their own
  `Name=`. Only the `[Desktop Entry]` group is read.

`NoDisplay` and `Hidden` entries are skipped, because those are plumbing the
desktop keeps out of menus and this is a menu. A `Terminal=true` application
says "Runs in a terminal", since launching one from a file manager usually
flashes and exits, and a choice that appears to do nothing is worse than one
that is labelled.

Scope, and why: the item is disabled for a pane pointed at a server (the path
names a file on that machine, and there is nothing here to hand to a local
application) and for directories. `handlerId` is validated as a bare
`*.desktop` filename in the main process and resolved only against the XDG
application directories, because a renderer that could pass a path there would
be choosing which program runs.

The list is a bounded flex column with the ScrollArea as `min-h-0 flex-1`,
not `max-h` on the ScrollArea root. The root form does not clip: the viewport
is `size-full`, so with no definite height it grows to fit and the last rows
are cut off by the dialog with no way to scroll to them. That is the same
trap fixed in the preview dialog earlier today, found the same way, by
looking at it.

Verified in headless Chromium under the app's real CSP, both themes: the menu
item appears on a local file and is disabled on a directory, the dialog lists
the handlers with the default marked, Open with nothing chosen sends a null
handler, and choosing Visual Studio Code sends code.desktop.

Handler enumeration itself could not be exercised end to end here: this box
has gio but no registered desktop associations, so `gio mime` returns nothing
for every type. The parsers are tested against real gio output and real
desktop files; the live lookup is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52
@ralyodio
ralyodio merged commit 9b3c0a1 into main Sep 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant