Skip to content

Open local HTML files instead of erroring - #1

Open
gamellis wants to merge 3 commits into
mainfrom
claude/relative-html-pages-bug-c369b4
Open

Open local HTML files instead of erroring#1
gamellis wants to merge 3 commits into
mainfrom
claude/relative-html-pages-bug-c369b4

Conversation

@gamellis

@gamellis gamellis commented Aug 8, 2026

Copy link
Copy Markdown
Owner

The bug

open page.html in a terminal (or double-clicking an HTML file in Finder) put up:

The document "preview-shapes-overlay.html" could not be opened. ChromeTabFocus cannot open files in the "HTML text" format.

Why

Finicky is the default handler for public.html, so it receives the file open and hands the file — not a URL — to the default browser, ChromeTabFocus. The applet claimed only the http/https URL schemes and had no on open handler, so LaunchServices refused the handoff. Web links kept working, which is why this went unnoticed.

The fix

  • chrome-tab-focus.applescript: on open theFiles handler, plus a fileURLFor percent-encoder. Chrome reports tab URLs percent-encoded, so the encoding has to match or every open would make a duplicate tab instead of focusing the existing one. Encoding goes through od rather than AppleScript's id of c, since one AppleScript character can be several UTF-8 bytes.
  • install-chrome-tab-focus.sh: adds the file scheme and a CFBundleDocumentTypes claim for public.html/public.xhtml at rank Alternate — enough to accept Finicky's handoff, not enough to become the app Finder shows for .html files.
  • verify.sh: flags an applet built before this change. The symptom is specific and easy to misread as a Finicky problem.
  • README note.

Verified locally

Rebuilt the applet and tested against real Chrome (test tabs cleaned up after):

  • open "test page.html"file:///private/tmp/ftf/test%20page.html, no dialog
  • re-opening the same file focused the existing tab (tab count 10 → 10)
  • open a.html "café ü.html" → both opened; unicode encoded as cafe%CC%81%20u%CC%88.html, matching what Chrome reports
  • ./verify.sh passes

Reviewer notes

  • Because the source now has an on open handler, osacompile produces a droplet rather than an applet — the app's icon changes. Behaviour is otherwise the same.
  • Claiming the file: URL scheme is broader than strictly needed for the observed failure; it covers the case where a future Finicky hands off a file:// URL instead of a file. Happy to drop it if you'd rather keep the claim minimal.
  • Existing installs need ./install-chrome-tab-focus.sh re-run; verify.sh now says so.

🤖 Generated with Claude Code

Finicky is the default handler for public.html, so `open page.html` hands
the *file* — not a URL — to the default browser, which is ChromeTabFocus.
The applet claimed only http/https URL schemes and had no `on open`
handler, so LaunchServices refused: "ChromeTabFocus cannot open files in
the 'HTML text' format." Every local page was affected; web links were
unaffected, which is why this went unnoticed.

The applet now accepts files, converts each to a percent-encoded file://
URL (matching Chrome's own encoding so tab reuse works for local pages
too), and routes it through the same focus-or-open path. The installer
claims public.html/public.xhtml at rank Alternate — enough to accept the
handoff, not enough to become the app Finder shows for .html files.

verify.sh flags an applet built before this, since the symptom is
specific and easy to misread as a Finicky problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gamellis
gamellis marked this pull request as ready for review August 8, 2026 20:52
gamellis and others added 2 commits August 10, 2026 10:08
The encoder escaped every character outside the unreserved set. Chrome
does not: it keeps : @ ! , = + $ & ' ( ) * [ ] literal and escapes only
space, non-ASCII, and # ; ? % { }. So a file already open in a tab that
Chrome itself created -- from Finder, a link, a restored session -- never
matched, and opening it made a duplicate instead of focusing it. Verified
byte-exact against Chrome's own canonicalisation on 11 paths covering
every class of punctuation, spaces, CJK and an emoji.

It also spawned a shell per escaped character. A path of 100 non-ASCII
characters cost ~490ms, a realistic one ~50ms, and it made the README's
"nothing is passed to a shell" false. Foundation's percent-encoding is
one call: the same path now costs a fraction of a millisecond, and the
framework load is paid once at launch because the applet stays open.

NSURL's fileURLWithPath would have been the obvious choice and is wrong:
absoluteString uses the NFD filesystem representation, so "café" comes
back as cafe%CC%81 where Chrome reports caf%C3%A9.

Comparing file: URLs decoded is the belt to that braces -- the encoder
tracks a Chromium rule that could drift, and a decoded comparison still
matches if it does, including against tabs the previous version opened.

verify.sh grepped the whole plist as text for public.html, which passes
on an applet that merely mentions it in a type name or an inherited UTI
declaration. It now reads the keys LaunchServices actually reads, and
checks the compiled script has an open-documents handler at all -- a
plist claim with no handler leaves the event to time out, which looks
exactly like the click doing nothing. Tested against a good build, a
plist-claims-but-no-handler build, a pre-fix build and that UTI fooler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The NFD/NFC match worked, but by accident. `POSIX path of` yields the
decomposed filesystem form while a tab Chrome opened from Finder carries
the composed form from disk, and what reconciled them was the NSString ->
AppleScript text coercion quietly composing on the way out: the NSString
is length 20, the coerced text 19. Nothing in the code said so, and the
match had no business depending on a side effect of a type coercion.

precomposedStringWithCanonicalMapping makes it the stated behaviour. No
change in result today -- verified the two spellings still compare equal
with `considering diacriticals and case`, which disables AppleScript's
implicit normalization, so the composition is now ours rather than the
runtime's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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