DiskPush 0.4.0 lets you select a stack of files, right click, and open all of them with one application you pick once. That part is unremarkable. The interesting half is the other mode: open them one at a time, where the next file opens when you are finished with the previous one.
A folder of photos you want in front of you all at once. Twelve episodes you want in order. Same gesture, two completely different intentions, and a file manager that only does the first one is annoying in a way that is hard to name until you hit it.
The problem
To open the next episode when the last one is done, you have to know when it is done. Nothing tells you.
The obvious approach is to launch the player yourself and wait for the process. To do that you need the command, which lives in the Exec line of a .desktop file, which is not a command:
Exec=mpv --profile=fast %U
Exec=totem %U
Exec=env BAMF_DESKTOP_FILE_HINT=... /usr/bin/code %F
Those % codes are field codes. %f is one file path, %F is a list, %u is one URL, %U is a list of URLs, and there are a dozen more that have been deprecated over the years but still appear in files shipping today. Writing that parser means deciding what to do when an entry wants URLs and you have a path, when it wants a list and you have one file, when it has no code at all. It is a small amount of code and a large number of ways to hand an application the wrong thing.
The desktop already has a program that does this correctly, gio launch, so the sensible move is to use it. Except:
$ time gio launch player.desktop episode.mkv
0.017 total
It returns in seventeen milliseconds. It hands the file to the launcher and exits. Waiting for it tells you nothing at all.
The thing I noticed by accident
I was timing that command through a pipe:
$ time gio launch sleeper.desktop | tail -3
gio launch sleeper.desktop 0.017 total
tail -3 3.020 total
gio finished in 17ms. tail sat there for three seconds.
Because the application gio launched inherited the pipe. tail cannot finish until every writer has let go of the other end, and the last writer was the application itself, not gio.
That is the signal. You do not wait for the process you spawned. You wait for the pipes it left behind:
const child = spawn('gio', ['launch', desktop, file], { stdio: ['ignore', 'pipe', 'pipe'] })
let open = 2
const closed = () => { if (--open === 0) resolve() }
child.stdout.on('end', closed).resume()
child.stderr.on('end', closed).resume()
Measured against a three second application: gio exits at 17ms, the pipes close at 3018ms. No Exec parsing, no field codes, no guessing.
Where it does not work, which matters more
Plenty of applications keep one window for everything. Open a second file and it goes to the copy already running, and the process you started exits immediately having done its job. The pipes close in twenty milliseconds. As far as the pipe trick is concerned, you finished watching in twenty milliseconds.
If the code believed that, it would open episode two, then three, then all twelve, straight into the running player, which is precisely the thing this mode exists to prevent. The failure would be worse than not having the feature.
So a close under one second is not treated as "finished", it is treated as "cannot tell". The run stops there and offers a button:
That application keeps one window for everything, so DiskPush cannot tell when you are done. Press Open next when you want the following file.
One second is a deliberate gap rather than a round number. A real hand-off is tens of milliseconds. Somebody actually watching or reading something takes seconds at the very least. There is nothing in between worth worrying about.
Two fixtures that tested the opposite of what they looked like
Writing tests for this, I reached for the obvious fake player:
Exec=sleep 1
gio appends the file to the Exec line, so that ran sleep 1 /tmp/a.mkv, which dies immediately with "invalid time interval". The pipes closed in milliseconds, the code correctly read a hand-off, the series parked waiting to be advanced, and the test hung until it timed out. The fixture was exercising the branch it was written to prove was not taken.
Fixed that with a script that ignores its argument. The test still failed. Because sleep 1 is one second and the hand-off threshold was one and a half, so a correctly working slow application was still classified as a hand-off.
Both failures were the code doing exactly the right thing to a badly built fixture. Both were only visible because the assertion was about elapsed time and event order rather than "did it throw". A test that had only checked for an absence of errors would have passed and proved nothing.
DiskPush 0.4.0: https://diskpush.com/download
This post was drafted with AI assistance.
Top comments (0)