diff --git a/modules/assets/mc/mc.ext.ini b/modules/assets/mc/mc.ext.ini new file mode 100644 index 0000000..fade80a --- /dev/null +++ b/modules/assets/mc/mc.ext.ini @@ -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 & diff --git a/modules/home.nix b/modules/home.nix index 349b63d..95fc0fe 100644 --- a/modules/home.nix +++ b/modules/home.nix @@ -81,7 +81,7 @@ let ''; 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 # by name without wiring each through nix. @@ -521,7 +521,25 @@ 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 # 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; swallow_regex = "^(foot)$"; }; diff --git a/modules/mc.nix b/modules/mc.nix index 3d01edd..b2bdb9d 100644 --- a/modules/mc.nix +++ b/modules/mc.nix @@ -1,7 +1,9 @@ -# 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 -# other desktop tooling. The only personal value is the right-panel start dir, -# exposed as an option defaulting to the consumer's own home. +# Midnight Commander: TUI file manager. Its config (ini, panels, F2 user menu, +# mc.ext.ini file associations) is opinionated but generic, so it lives in the +# public baseline alongside the other desktop tooling. The only personal value +# 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 # the extension), so it ships with mc to keep the menu self-contained. @@ -37,16 +39,30 @@ 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 = { home.packages = [ pkgs.mc pkgs.ouch ]; local.mc.menu = lib.mkBefore (builtins.readFile ./assets/mc/menu); + local.mc.extIni = lib.mkBefore (builtins.readFile ./assets/mc/mc.ext.ini); xdg.configFile = { "mc/ini".source = ./assets/mc/ini; "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 # baking in a personal path. diff --git a/modules/mime.nix b/modules/mime.nix new file mode 100644 index 0000000..1fbdeaf --- /dev/null +++ b/modules/mime.nix @@ -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" + ]); +}