The Editor Wars
Thursday, 15 Oct 2026
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-allapplication/x-zerosize— empty files, becausefile(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
- write
~/.config/mimeapps.listand~/.local/share/applications/mimeapps.list - purge the offending package
- fix the Firefox handler via the “Always Open” context menu, once per content type
- 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.