Skip to content

feat(desktop): open a whole selection, together or one at a time - #33

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

feat(desktop): open a whole selection, together or one at a time#33
ralyodio merged 1 commit into
mainfrom
feat/open-multiple

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

"Open with" now acts on the selection rather than the one row you happened to
right-click, and the handler is chosen once for all of it. Being asked which
application to use twelve times in a row is not a feature.

Two modes, because a set of files is not one kind of thing:

  • All at once hands every file to the application in a single invocation,
    so an entry taking %F or %U gets them together, which is what "open all of
    them" means to the application as well as to the person asking.
  • One at a time opens a file, waits until the application is finished with
    it, and then opens the next. A series of episodes is watched in order and
    opening twelve at once is never what anyone meant.

The default is picked from what the files are: video or audio defaults to one
at a time, everything else to all at once, and a single file is not offered a
mode at all. A mixed selection has no shared type, so it gets all at once and
each file opens in its own default application.

Waiting for an application to finish, without reimplementing Exec.
gio launch returns almost immediately (measured at 17ms): it hands the file
to the desktop's launcher and exits. But the application it starts inherits
our stdio, so the PIPES close when the application does, measured at 3018ms
for a three second app. Waiting on the pipes rather than the process is
therefore a reliable "done with this file" signal, and it costs nothing: the
alternative is parsing the desktop entry's Exec field codes (%f, %F, %u, %U)
ourselves, which is a small parser and a large number of ways to hand an
application the wrong thing.

The case that breaks it, handled rather than ignored. A single-instance
application hands the file to the copy already running and exits at once, so
there is no "it closed" to wait for. A close under one second is read as that
rather than as somebody watching an episode in under a second: the run parks
and offers Open next instead of advancing. Auto-advancing there would dump the
whole list into the running player, which is the exact thing this mode exists
to prevent. The dialog says so in as many words rather than looking stalled.

Both fixture bugs found while testing this are worth knowing, and are recorded
in the test that hit them:

  • Exec=sleep 1 does not work as a fake player, because gio appends the file
    to the Exec line and sleep 1 /tmp/a.mkv dies instantly with "invalid time
    interval". The code then correctly reads that as a hand-off, so the naive
    fixture tests the opposite of what it appears to.
  • A one second app is under the hand-off threshold. The fixture has to be
    clearly above it or the test asserts the wrong branch. That also moved the
    threshold from 1500ms to 1000ms, which sits well clear of a real hand-off
    (tens of ms) and nowhere near somebody actually using a file.

Verified in headless Chromium under the app's real CSP: selecting three .mkv
files names the count in the menu ("Open 3 files with…"), the dialog defaults
to one at a time because they are video, the run carries all three paths and a
null handler, the hand-off parks with "1 of 3" and an explanation, Open next
advances exactly once, and a lone markdown file is offered no mode at all.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52

"Open with" now acts on the selection rather than the one row you happened to
right-click, and the handler is chosen once for all of it. Being asked which
application to use twelve times in a row is not a feature.

Two modes, because a set of files is not one kind of thing:

- **All at once** hands every file to the application in a single invocation,
  so an entry taking %F or %U gets them together, which is what "open all of
  them" means to the application as well as to the person asking.
- **One at a time** opens a file, waits until the application is finished with
  it, and then opens the next. A series of episodes is watched in order and
  opening twelve at once is never what anyone meant.

The default is picked from what the files are: video or audio defaults to one
at a time, everything else to all at once, and a single file is not offered a
mode at all. A mixed selection has no shared type, so it gets all at once and
each file opens in its own default application.

**Waiting for an application to finish, without reimplementing Exec.**
`gio launch` returns almost immediately (measured at 17ms): it hands the file
to the desktop's launcher and exits. But the application it starts inherits
our stdio, so the PIPES close when the application does, measured at 3018ms
for a three second app. Waiting on the pipes rather than the process is
therefore a reliable "done with this file" signal, and it costs nothing: the
alternative is parsing the desktop entry's Exec field codes (%f, %F, %u, %U)
ourselves, which is a small parser and a large number of ways to hand an
application the wrong thing.

**The case that breaks it, handled rather than ignored.** A single-instance
application hands the file to the copy already running and exits at once, so
there is no "it closed" to wait for. A close under one second is read as that
rather than as somebody watching an episode in under a second: the run parks
and offers Open next instead of advancing. Auto-advancing there would dump the
whole list into the running player, which is the exact thing this mode exists
to prevent. The dialog says so in as many words rather than looking stalled.

Both fixture bugs found while testing this are worth knowing, and are recorded
in the test that hit them:

- `Exec=sleep 1` does not work as a fake player, because gio appends the file
  to the Exec line and `sleep 1 /tmp/a.mkv` dies instantly with "invalid time
  interval". The code then correctly reads that as a hand-off, so the naive
  fixture tests the opposite of what it appears to.
- A one second app is *under* the hand-off threshold. The fixture has to be
  clearly above it or the test asserts the wrong branch. That also moved the
  threshold from 1500ms to 1000ms, which sits well clear of a real hand-off
  (tens of ms) and nowhere near somebody actually using a file.

Verified in headless Chromium under the app's real CSP: selecting three .mkv
files names the count in the menu ("Open 3 files with…"), the dialog defaults
to one at a time because they are video, the run carries all three paths and a
null handler, the hand-off parks with "1 of 3" and an explanation, Open next
advances exactly once, and a lone markdown file is offered no mode at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VScug5VRbcTuhiAoieeQ52
@ralyodio
ralyodio merged commit 3585d38 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