feat(mc): open files with their configured app via xdg-open

Ship `mc.ext.ini` as the `local.mc.extIni` option (like `local.mc.menu`):
Enter browses archives in-place as before and delegates every other type to
`xdg-open`. Add public `mime.nix` with `xdg.mimeApps` defaults (pdf ->
zathura, images -> imv, audio/video -> mpv, office -> libreoffice) -- public
because those handlers are; personal browser/mail defaults stay private.

Also document, at the Hyprland swallow config, how swallowing differs from an
app opening fullscreen/maximized (LibreOffice restores its own maximized state
and looks swallowed but isn't).
This commit is contained in:
2026-07-12 22:09:01 +02:00
parent 7205841c4d
commit 4be1c4fb67
4 changed files with 196 additions and 6 deletions
+81
View File
@@ -0,0 +1,81 @@
# Midnight Commander file associations (public baseline).
#
# mc reads a SINGLE mc.ext.ini; this user copy fully replaces the system one
# (they are not merged), so it must also carry the archive entries we keep.
#
# Policy: Enter (Open) on an archive mounts it as a virtual filesystem (mc
# built-in, no external helper). Every other file falls through to [Default],
# which runs `xdg-open` -- so the app is whatever the XDG mimeapps default
# resolves to (set those in the private inventory's xdg.mimeApps). One source of
# truth, shared with every other launcher; nothing app-specific baked in here.
#
# Note: office documents (.docx/.odt/.xlsx/...) are ZIP containers, so they are
# deliberately absent from the [zip] entry below -- otherwise Enter would mount
# them as archives instead of handing them to LibreOffice via xdg-open.
#
# Override contract (this is a types.lines option shipped as mkBefore): append a
# same-named section to override one key -- mc merges same-named sections and
# the last key wins -- e.g. add a [Default] with a different Open to change the
# fallback, or add [pdf] Type=^PDF / Open=mupdf %f & to force one type to a
# specific app. lib.mkForce "..." replaces this baseline wholesale.
[mc.ext.ini]
Version=4.0
### Archives: Enter mounts them as a virtual filesystem (F3/F4 unaffected) ###
[tar]
Regex=\\.(tar(\\.(gz|bz2|xz|zst|lz|lz4|lzo|Z))?|t(gz|az|bz2?|xz|zst|lz))$
Open=%cd %p/utar://
[zip]
Regex=\\.(zip|jar|xpi|whl|egg|apk|war|ear)$
Open=%cd %p/uzip://
[7z]
Shell=.7z
Open=%cd %p/u7z://
[rar]
Regex=\\.r(ar|[0-9][0-9])$
Open=%cd %p/urar://
[arj]
Regex=\\.a(rj|[0-9][0-9])$
Open=%cd %p/uarj://
[cab]
Shell=.cab
Open=%cd %p/ucab://
[cpio]
Regex=\\.cpio(\\.(gz|xz|bz2|zst|lz|lz4|lzo|Z))?$
Open=%cd %p/ucpio://
[deb]
Regex=\\.u?deb$
Open=%cd %p/deb://
[rpm]
Regex=\\.(rpm|src\\.rpm|spm)$
Open=%cd %p/rpm://
[iso]
Shell=.iso
Open=%cd %p/iso9660://
[patch]
Regex=\\.(diff|patch)(\\.(gz|bz2|xz|zst|Z))?$
Open=%cd %p/patchfs://
### Everything else: hand off to the desktop's configured default app ###
# `&` backgrounds xdg-open so mc's shell returns and the launched GUI is orphaned
# to init. Hyprland only swallows the terminal when the new window's parent-PID
# chain still has a foot ancestor (getSwallower in Hyprland's Window.cpp), so an
# orphaned app opens as its own tile instead of swallowing mc (zathura, imv, ...).
# %f MUST stay unquoted: mc already backslash-escapes the path, so quoting turns
# those backslashes literal and breaks filenames with spaces.
[Default]
Open=xdg-open %f &
+20 -2
View File
@@ -81,7 +81,7 @@ let
''; '';
in in
{ {
imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ]; imports = [ ./gpg.nix ./media.nix ./texlive.nix ./librewolf.nix ./mc.nix ./mime.nix ];
# Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run # Put ~/.local/bin (the XDG user-bin dir) on PATH so hand-dropped scripts run
# by name without wiring each through nix. # by name without wiring each through nix.
@@ -521,7 +521,25 @@ in
# Matches on the *parent* terminal's class. Needs standalone foot: in # Matches on the *parent* terminal's class. Needs standalone foot: in
# server/client mode all windows share one process, so PID ancestry can't # server/client mode all windows share one process, so PID ancestry can't
# tell them apart and swallowing breaks — that's why $mod+Return runs # tell them apart and swallowing breaks — that's why $mod+Return runs
# `foot`, not `footclient`. # `foot`, not `footclient`. Swallow is decided by getSwallower() walking
# the new window's parent-PID chain (up to 25 hops) for a window whose
# class matches swallow_regex; swallow_exception_regex, if set, matches the
# *terminal's title*, not the new window's class (a common misreading).
#
# Do NOT confuse swallowing with an app opening fullscreen/maximized — they
# look identical (the launching terminal vanishes) but are different things:
# - real swallow removes the terminal from the layout and restores it only
# when the swallower closes; you cannot un-swallow a live window.
# - a fullscreen/maximized app merely covers the workspace; the launcher
# is still a tile underneath and $mod+f / $mod SHIFT f toggles it back.
# `hyprctl clients` is decisive: a real swallow shows the terminal
# hidden=true and the new window swallowing!=0x0. LibreOffice trips this —
# it restores its own maximized state on open (from mc or a shell), so a
# document appears to "swallow" mc when it is only fullscreen. And being
# single-instance (one soffice.bin owns every window via IPC handoff), when
# it genuinely does swallow, that is fixed by whatever launched its daemon,
# not per-open — so it can't do the tile-from-mc / swallow-from-shell split
# that single-process handlers like zathura/imv get for free.
enable_swallow = true; enable_swallow = true;
swallow_regex = "^(foot)$"; swallow_regex = "^(foot)$";
}; };
+20 -4
View File
@@ -1,7 +1,9 @@
# Midnight Commander: TUI file manager. Its config (ini, panels, F2 user menu) # Midnight Commander: TUI file manager. Its config (ini, panels, F2 user menu,
# is opinionated but generic, so it lives in the public baseline alongside the # mc.ext.ini file associations) is opinionated but generic, so it lives in the
# other desktop tooling. The only personal value is the right-panel start dir, # public baseline alongside the other desktop tooling. The only personal value
# exposed as an option defaulting to the consumer's own home. # is the right-panel start dir, exposed as an option defaulting to the
# consumer's own home. mc.ext.ini keeps archive browsing but routes every other
# type to xdg-open, so the app choices live in xdg.mimeApps (baseline: mime.nix).
# #
# ouch backs the F2 pack/unpack entries (one tool, archive format inferred from # ouch backs the F2 pack/unpack entries (one tool, archive format inferred from
# the extension), so it ships with mc to keep the menu self-contained. # the extension), so it ships with mc to keep the menu self-contained.
@@ -37,16 +39,30 @@
lib.mkForce to replace it entirely. lib.mkForce to replace it entirely.
''; '';
}; };
extIni = lib.mkOption {
type = lib.types.lines;
description = ''
Contents of mc's mc.ext.ini (file-type associations driving Enter/F3/F4).
The shipped baseline keeps mc's archive browsing and delegates every
other type to xdg-open, so the actual app is the XDG mimeapps default.
Like `menu`, it is a mkBefore definition: append a same-named section to
override one key (mc merges same-named sections, last key wins), or use
lib.mkForce to replace it entirely.
'';
};
}; };
config = { config = {
home.packages = [ pkgs.mc pkgs.ouch ]; home.packages = [ pkgs.mc pkgs.ouch ];
local.mc.menu = lib.mkBefore (builtins.readFile ./assets/mc/menu); local.mc.menu = lib.mkBefore (builtins.readFile ./assets/mc/menu);
local.mc.extIni = lib.mkBefore (builtins.readFile ./assets/mc/mc.ext.ini);
xdg.configFile = { xdg.configFile = {
"mc/ini".source = ./assets/mc/ini; "mc/ini".source = ./assets/mc/ini;
"mc/menu".text = config.local.mc.menu; "mc/menu".text = config.local.mc.menu;
"mc/mc.ext.ini".text = config.local.mc.extIni;
# Generated (not a static asset) so other_dir tracks the option instead of # Generated (not a static asset) so other_dir tracks the option instead of
# baking in a personal path. # baking in a personal path.
+75
View File
@@ -0,0 +1,75 @@
# Default applications per file type -- the single source of truth for "open
# with" across the desktop: xdg-open, file pickers, and Enter in mc (whose
# mc.ext.ini delegates every non-archive type to xdg-open; see mc.nix).
#
# These belong in the public baseline because the handlers do too: zathura, imv
# and mpv come from media.nix, libreoffice from home.nix. So "PDF opens in
# zathura", "images in imv", etc. are the natural defaults of this baseline, not
# personal choices -- every entry is mkDefault, so a consumer overrides one type
# by setting the same key (their definition wins), or flips enable off entirely.
#
# Personal handlers -- web browser, mail -- are NOT here; they map to a specific
# browser profile / mail client and live in the private inventory, merging into
# the same defaultApplications attrset.
{ lib, ... }:
let
# Map every mime in the list to one .desktop id, at mkDefault priority.
forAll = desktop: mimes: lib.genAttrs mimes (_: lib.mkDefault desktop);
zathura = "org.pwmt.zathura.desktop";
imv = "imv.desktop";
mpv = "mpv.desktop";
writer = "writer.desktop";
calc = "calc.desktop";
impress = "impress.desktop";
in {
xdg.mimeApps.enable = lib.mkDefault true;
xdg.mimeApps.defaultApplications =
(forAll zathura [
"application/pdf"
"application/epub+zip"
"application/oxps"
"application/postscript"
])
// (forAll imv [
"image/png"
"image/jpeg"
"image/gif"
"image/webp"
"image/bmp"
"image/tiff"
])
// (forAll mpv [
"video/mp4"
"video/x-matroska"
"video/webm"
"video/x-msvideo"
"video/quicktime"
"video/mpeg"
"audio/mpeg"
"audio/flac"
"audio/ogg"
"audio/x-wav"
"audio/mp4"
"audio/x-opus+ogg"
])
// (forAll writer [
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
"application/msword"
"application/vnd.oasis.opendocument.text"
"application/rtf"
])
// (forAll calc [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
"application/vnd.ms-excel"
"application/vnd.oasis.opendocument.spreadsheet"
])
// (forAll impress [
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
"application/vnd.ms-powerpoint"
"application/vnd.oasis.opendocument.presentation"
]);
}