Random Musings

O for a muse of fire, that would ascend the brightest heaven of invention!


The Editor Wars

Thursday, 15 Oct 2026 Tags: desktop freebsd

Every year or so a fresh editor installs itself, and my .md and .txt files stop opening in Neovim. Last time it was LibreOffice grabbing the PDFs and a Zed fork called Gram grabbing the markdown. The fix is a handful of files nobody remembers they have, so here it is written down.

What actually decides this

FreeBSD desktops use the XDG MIME machinery — the same .desktop files that populate the application menu also advertise which MIME types an application claims. Each .desktop entry has a MimeType= line, and the type a file is given comes from file(1)-style matching in the shared-mime-info database.

When you double-click a file, the file manager looks up a default handler for that type. The lookup order matters:

~/.config/mimeapps.list
~/.local/share/applications/mimeapps.list
/usr/local/share/applications/mimeapps.list    (or {XDG dirs}/mimeapps.list)

~/.config/mimeapps.list wins. If the type is not listed in any of them, the system falls back to the highest-priority .desktop file that claims the type, which is exactly how a new package silently takes over: pkg install some-editor drops a .desktop that claims text/plain, and with no explicit default recorded, that editor is now the winner.

xdg-mime default <app.desktop> <type> is supposed to write these files for you. On this box it returned success and wrote nothing, so the files get edited directly. It is a key=value file; nothing more.

What a new editor does

A new editor ships a .desktop file that claims:

  • text/plain — every editor claims this, it is the plain-text catch-all
  • application/x-zerosize — empty files, because file(1) calls a 0-byte file its own type
  • optionally a wildcard like text/*, which claims every text subtype that has no more specific handler

The subtle part is text/markdown. Neovim’s own .desktop does not list text/markdown; it lists text/plain and a pile of code types. So my markdown files were never opening in Neovim because of an explicit rule — they opened there because markdown falls back to the text/plain handler. The moment a new text/plain claimant lands, every markdown file follows it. Fixing this means writing text/markdown=nvim-qt.desktop explicitly, not just assuming it is covered.

The two Neovim entries

nvim.desktop is the terminal one (Terminal=true, Exec=nvim %F). nvim-qt.desktop is the GUI (Exec=nvim-qt -- %F). They claim identical MIME lists. I want the GUI, so every entry points at nvim-qt.desktop.

The firefight

The mimeapps.list files now look like this, code and plain text going to the GUI, rich text and spreadsheets staying where they belong:

[Default Applications]
application/pdf=katarakt.desktop
application/x-shellscript=nvim-qt.desktop
text/comma-separated-values=libreoffice-calc.desktop
text/csv=libreoffice-calc.desktop
text/english=nvim-qt.desktop
text/markdown=nvim-qt.desktop
text/mathml=libreoffice-math.desktop
text/plain=nvim-qt.desktop
text/rtf=libreoffice-writer.desktop
text/spreadsheet=libreoffice-calc.desktop
text/tab-separated-values=libreoffice-calc.desktop
text/vnd.trolltech.linguist=linguist.desktop
text/x-c=nvim-qt.desktop
text/x-c++=nvim-qt.desktop
text/x-c++hdr=nvim-qt.desktop
text/x-c++src=nvim-qt.desktop
text/x-chdr=nvim-qt.desktop
text/x-csrc=nvim-qt.desktop
text/x-java=nvim-qt.desktop
text/x-log=nvim-qt.desktop
text/x-makefile=nvim-qt.desktop
text/x-markdown=nvim-qt.desktop
text/x-moc=nvim-qt.desktop
text/x-pascal=nvim-qt.desktop
text/x-tcl=nvim-qt.desktop
text/x-tex=nvim-qt.desktop

The CSV types stay on libreoffice-calc despite starting with text/, because Calc holds them legitimately and an editor is the wrong tool for a spreadsheet. text/rtf and text/mathml go to Writer and Math; raw text in a Rich Text file is not the goal.

Firefox is its own planet

Firefox does not read mimeapps.list at all. It keeps a per-profile handlers.json with its own MIME-to-application map, independent of the desktop. That is why PDFs kept opening in LibreOffice inside the browser long after katarakt was set everywhere else.

"mimeTypes": {
  "application/pdf": {
    "action": 0,
    "handlers": [{"name": "katarakt", "path": "/usr/local/bin/katarakt"}],
    "extensions": ["pdf"]
  }
}

The gotcha, learned the hard way, is that hand-editing handlers.json does not stick. Firefox rewrites the file on exit and turns the action field from 0 (use the listed handler) to 2 (ask the OS), at which point the download falls back to whatever the desktop resolves — LibreOffice again. The file is also keyed per download content type: a PDF served as application/octet-stream with a Content-Disposition: attachment does not use the application/pdf entry at all.

The reliable fix is the browser’s own UI, not the file:

download a PDF
open the Downloads list (Ctrl+J)
right-click the file, "Always Open in Katarakt PDF Viewer"

That writes the association through Firefox’s own code path and survives a restart. It also has to be done per content type — a PDF arriving as octet-stream needs its own “Always Open” once, because Firefox treats it as a different type from application/pdf. The same “Always Open” option sits under Settings → General → Applications for each content type that has ever been seen.

The handler itself must also exist and be readable. katarakt.desktop was 600 root-owned on this box once, so the desktop could not resolve it and silently fell through to LibreOffice; once it was 644 and carried MimeType=application/pdf, the “Always Open” action had a valid target.

Profiles live under ~/.mozilla/firefox/*/, and there can be several; profiles.ini says which is Default. Each has its own handlers.json.

Purging the intruders

The .desktop files are installed by packages, so removing them means removing the package:

pkg delete gram-text-editor zed-editor

pkg which /usr/local/bin/gram and pkg which /usr/local/share/applications/whatever.desktop tell you the owning package when the name is not obvious. Once the package is gone, its .desktop file goes with it and the application stops being a candidate for text/plain.

Order of operations, since it matters

  1. write ~/.config/mimeapps.list and ~/.local/share/applications/mimeapps.list
  2. purge the offending package
  3. fix the Firefox handler via the “Always Open” context menu, once per content type
  4. log out and back in

Steps 1 and 2 fix the desktop; step 3 fixes the browser. They are independent, but both must happen or the browser and the desktop will continue to disagree.