____________________________________________________________________________ | | | ____ _____ | | | _ \ ___ ___ _ _| __/___ ___ _____ ___ ___ | | | |_| / _ \/ __| |/ | |_ / _ \| _ |_ _| |_ _/ _ \ | | | _ | |_|| |__| ' <| _| |_| | / | | _ | | |_| | | | |_| \_\___/\___|_|\_|_| \___/|_|_\ |_| |_| |___\___/ | | | |__________________________________________________________________________|
____ _____ | _ \ ___ ___ _ _| __/___ ___ _____ ___ ___ | |_| / _ \/ __| |/ | |_ / _ \| _ |_ _| |_ _/ _ \ | _ | |_|| |__| ' <| _| |_| | / | | _ | | |_| | |_| \_\___/\___|_|\_|_| \___/|_|_\ |_| |_| |___\___/

Midnight Commander customizations (Part 2) ==========================================

First draft: 2026-01-10
Published:   2026-02-24

Table of contents:

Join me as I document my recent efforts to make life using MC more comfortable.

This is Part 2: User Menu improvements.
Take a look at Part 1 if interested in file highlight and extension configuration.

TL;DR: See the full `menu` code on GitHub.

Licensing note: The config file and snippets in this article are licensed under GPL-⁠3.0-⁠or-⁠later. The original is based on configuration defaults distributed with version 4.8.33 of GNU Midnight Commander. Please refer to the official MC GitHub repository.

Overview

When you press F2 in MC, the User menu pops up, which offers a couple of handy shortcuts out of the box, but more importantly its items can be customized through a config file. Invoking the "Edit menu file" option in Command menu, we are presented with the choice of opening either the Local or User file: former refers to `~./⁠config/⁠mc/⁠.mc.menu`, while the latter to `~/⁠.config/⁠mc/⁠menu`. I am not sure in what circumstances would be advantageous to have both of these with different contents, so I am just sticking with the one for the User. We get a short description of the format in the file header, but it might be worth pointing out that there is also a section dedicated to this in the manual, see Edit Menu File.

I know this has been mentioned in the previous post, but for those only interested in the user menu, it is worth mentioning that if the selected local configuration file does not exist, the system-wide global instance will be copied from `/⁠etc/⁠mc/⁠`. When invoking as root, we are presented with the additional choice to open the system global directly. I would like to point out that before editing the the system-wide instance, it could be advantageous to make a backup, because I could not find the defaults stored under `/⁠usr/⁠share/⁠mc/⁠` (unlike for the skins).

In the following sections I will go over my additions to the user menu. The patch format would be too distracting here, so I will just list the code one by one for these entries. There are other changes to the default contents, for instance you will see that some of mine override the hotkey of a previously existing option; so be sure to check the conclusion at the end and of course the full code for total clarity. Note that MC executes these sequences under `sh` so we have to stick with the POSIX shell syntax.

Introduction & trashing

The following moves the current file to a dedicated TRASH directory:

> + ! t t
> `       Move current file to TRASH
>         mkdir -p ~/TRASH
>         mv -v --backup=t %p ~/TRASH/

It's counterpart does the same thing, just to all the files that are tagged:

> + t t
> `       Move tagged files to TRASH
>         mkdir -p ~/TRASH
>         for i in %t ; do
>             mv -v --backup=t "$i" ~/TRASH/
>         done

This first one is pretty simple so I am going over now line-by-line describing how this works, and I can omit this kind of detailing in the following sections. First and foremost, take note that this is really not two items but the same one in two slightly different form. Most important line here is the second one which is the title; the first is just an annotation, and all the rest are the script body itself, commands which will be run in a /⁠bin/⁠sh subshell. The title starts with exactly one character, which will become the shortcut key of the entry inside the User Menu, and the short description that follows will also be displayed as-is on runtime (but may be truncated if too long).

Annotations starting with a '+' plus sign insert an addition condition, meaning the entry will be added to the menu only if the conditions evaluate as True. Naturally, defining two commands with the same hotkey can only work well if their conditions are mutually exclusive. A `t t` condition is True if there are any files tagged (as in "marked" or "highlighted" with Insert) in the active pane, and with an '!' exclamation mark we can invert the condition. In many cases it can be handy to have the command set up both for tagged and just the current file. Most of the time the tagged variant will execute the same thing just inside a loop, but sometimes we want other subtle alterations as well.

With the introductions out of the way, let's consider our first example itself. There are many ways to have a "Trashing" system set up on a machine, and I like mine the simplest which is just another regular directory dedicated for this, without further automation. I do not have any statistics on this, but considering my daily work, I would think that I have about 50% chance that I know at the moment of deletion that I will not be needing the file ever again, because it may be some kind of temporary artifact. Since the old times with Windows Commander, I am just used to taking into consideration every time if I might want to hold Shift down with F8 or not. As F18 is not supported in MC at this time (would be a nice feature for me obviously), this is my way of doing the same.

First we are making sure the TRASH directory exists using the `-⁠p` option of `mkdir`, then we move the current file (getting the filename with `%p`) there. The `-⁠-⁠backup=t` option for `mv` ensures that if there is a file in TRASH with the same name already, it will not be overwritten but kept with a numbered extension, without loss and with arguably a lesser annoyance of having to sort out which one to restore if the time comes.

Just one last word on why I chose the '`' backtick character for this (also known as grave accent). I wanted to have this theme of using numbers 1-⁠4 in a similar or relatable fashion as F1-⁠F4 (as you will see shortly), but 8 is very awkward in this way. I think on most standard keyboards (but definitely on mine), due to the space between F4 and F5, Number 8 are not located directly below F8 in the same way as 4 is under F4 for example. So I thought let's not use 8 because nobody wants to "fish around" keys in the dark while performing such a critical operation as deleting files. The backtick is in a very convenient position in this regard. Fun fact, while I have US key mapping set up as my default, I always have a keyboard with Hungarian layout physically, and we conveniently have Number Zero here (but I think this is common on other 105-⁠key QWERTZ layouts as well). To be honest I have never really understood the positioning of the backtick: it is such a prominent place for such a rarely used character, even compared to its alternate the '~' tilde...

Man

With the backtick explained, next we continue rightward on the keyboard towards those items somewhat analogous to F1-⁠F4. I am mentioning Number 1 just quickly here, because it is in fact 'm' from the default file reassigned.

> #m       View manual page
> 1       View manual page
>         MAN=%{Enter manual name}
>         %view{ascii,nroff} MANROFFOPT='-c -Tlatin1' MAN_KEEP_FORMATTING=1 man -P cat "$MAN"

I think it is obvious how this is similar to invoking the Help. I still prefer calling `man` by hand on the Ctrl-⁠O shell, but this is handy in those situations when I am working on something there already, there is no need to fire up a separate terminal. Alternatively, when I have a "dedicated" man window already which is a common scenario, this allows having two man pages to be open at the same time, and switch with Ctrl-⁠O.

Xdg-open

One annoyance when using MC could be, as it was mentioned earlier briefly when talking about the extension config, that simply hitting Enter on a regular file (i.e. not an executable) of a known type opens it in the foreground. Of course this is what we want in many cases, but in others - especially when the associated program is graphical - it would be more advantageous to launch in the background with the '&' ampersand, and still be able to use MC in the meantime while the file is still open.

I have made the choice of sticking with foreground on regular Open action, as this is how most of the stuff works already in `mc.ext.ini`, and providing a separate user menu item for launching in the background.

> + t rl
> 2       Open current file in the background with xdg-open
>         xdg-open %p &

With the `t rl` condition we can restrict this menu entry to only be shown for regular files and links.

I employed the `xdg-⁠open` command so it has other benefits as well, apart from it clearly implying that a graphical application will be launched. The file associations used here - at least on my Debian system - are coming from `~/⁠.config/⁠mimeapps.list`, which can also be managed through the `xdg-⁠mime` command. This is basically a key-value table defining relations between mime-type and .desktop entries. The latter are usually installed by the package manager in `/⁠usr/⁠share/⁠applications/⁠`, though we can make our own as well for packages installed through some alternate way, and put them in `~/⁠.local/⁠share/⁠applications/⁠`.

As an example, we can pull the .desktop file from an AppImage pretty easily, after extracting its contents by executing it with the `-⁠-⁠appimage-⁠extract` option. Usually we just need to adjust the `Exec` key to point to the AppImage itself, or what I like to do is to create a link in `~/⁠.local/⁠bin/⁠` and use that. This way I only need to update the link and not the .desktop when I start using a new version (I like to keep earlier versions around as backup if a new one fails to open my old saves).

Naturally, be mindful if you use MC as root, because launching graphical applications with elevated privileges, especially in the background can lead to severe consequences. Maybe this menu item should be disabled altogether for the superuser. Also obviously if MC is used in a headless environment, we could save some space by removing this and other entries that launch graphical applications (like Meld as we will see shortly).

File

"But how do I determine the mime-type?", I hear you ask. With Number 3 item we can invoke the `file` command to view its type.

> 3       View file type using the `file` command
>         %view{ascii}
>             echo "$(realpath -s %p):\n"
>             file -bi %p
>             file -b %p
>             if test -L %p ; then
>                 echo ""
>                 REAL=`realpath %p`
>                 file -bi "$REAL"
>                 file -b "$REAL"
>             fi

To illustrate, we get the following on the `menu` file itself:

> /etc/mc/mc.menu:
>
> text/plain; charset=us-ascii
> ASCII text

With `%view` we ask for all the output to be piped into MC's internal file viewer. Without this the file browser panes would pop back up after execution, and we would have to Ctlr-⁠O to see the output.

There are two other things to explain here. If the current file is a link, we would get `inode/⁠symlink` or similar as type, which might not be satisfactory. Hence we test for links and execute `file` on the `realpath` as well. Also we are running it twice to have both the human readable description, and the exact mime-type as output, latter activated by `-⁠i`. Here `-⁠b` just disables the default behavior of prepending the output with the filename, which we printed earlier.

As an example for links, here is the output for one in `/⁠etc/⁠`:

> /etc/os-release:
>
> inode/symlink; charset=binary
> symbolic link to ../usr/lib/os-release
>
> text/plain; charset=us-ascii
> ASCII text

Empty file

The following is my alternative for F4, "Edit".

> 4       Create an empty file
>         FILE=%{Enter name for new file}
>         [ -e "$FILE" ] && echo "Error: '$FILE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         touch $FILE

The `%{}` syntax might be new: it is a trigger for a graphical input dialog within MC, which is just a nicer (although less flexible) alternative for a command line `read` prompt. Incidentally we are using `read` here later as well, just to pause in case of an error. This prevents the panes popping back up again, which could leave the user wondering what happened. The `%view` solution would be cleaner but of course if there is no error, we *do* want to get back to the panes at once.

Otherwise I think the intent here is pretty straight-forward, with the only comment that best would be a way to launch an editor right away. Unfortunately I could not find a good way for that yet, that respects the user's default editor setting in MC. Of course we have the F14 shortcut which just launches an empty editor, and basically the same thing could be achieved simply by saving there right away; but I have found it goes against my general flow of thought. Usually I have the intended filename ready in my mind when starting editing, and even if not, this method would force me to think of one beforehand, so I would not forget the directory context, and could deal with clashes far better.

Compress subdirectory or tagged files

In the default user menu, we have no less that 6 different "Compress the current subdirectory" items (Numbers 3 though 8), with various compression methods. I can understand that this is to appeal to a wide audience where everyone can find their favorite among what is available. But if we turn this the other way around, for any one person there will be 4 or 5 choices here that are not used. That is what got me started to think if I could make these more useful for myself, also two choices were missing entirely which are lzip and plain tar. There are other reasons too: as it is evident from the entries that were discussed earlier, Numbers 3 and 4 got reassigned, so I thought I should use Number 9 and work backwards from there.

I have kept tar.gz as the first choice but made some improvements:

> #= t d
> #3       Compress the current subdirectory (tar.gz)
> #        Pwd=`basename %d /`
> #        echo -n "Name of the compressed file (without extension) [$Pwd]: "
> #        read tar
> #        [ "$tar"x = x ] && tar="$Pwd"
> #        cd .. && \
> #        tar cf - "$Pwd" | gzip -f9 > "$tar.tar.gz" && \
> #        echo "../$tar.tar.gz created."
>
> = t d
> + ! t t
> 7       Compress the current subdirectory (tar.gz)
>         Pwd=`basename %d /`
>         echo -n "Name of the compressed file (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar.gz"
>         [ -e "../$ARCHIVE" ] && echo "Error: '../$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         cd .. && \
>             tar -cz --posix -f "$ARCHIVE" "$Pwd" && \
>             echo "../$ARCHIVE created."

The differences compared to the default implementation (which is in comments now) are the following. There is a condition for the tagged case, thus we will have two separate variants now. The name for the archive got brought out to a variable as we refer to it more times now. Next we check if the archive exists already, so we can throw an error. Lastly, I have tweaked the invocation of tar a little bit: I had found the choice of using Level 9 compression with gzip a little odd considering that it is not used at the "Gzip or gunzip current file" menu items (see 'y' and 'Y'); also I opted to switch to the `posix` format explicitly in order to be compatible with `tarlz` (more on this shortly), and I believe this can benefit portability as well.

I wanted to add a variant as well that works on tagged files rather than the whole working directory:

> + t t
> 7       Compress tagged files (tar.gz)
>         set -- %t
>         CNT=$#
>         case $CNT in
>             1) Pwd=$1;;
>             *) Pwd=`basename %d /`;;
>         esac
>         echo -n "Name of the compressed file (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar.gz"
>         [ -e "$ARCHIVE" ] && echo "Error: '$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         tar -cz --posix -f "$ARCHIVE" %t && \
>             echo "$ARCHIVE created."

There are two major differences here. Near the start we extract the item count of our tagged files (`%t` being a space-separated list in itself) and conditionally set the filename recommendation accordingly, which is just a convenience in the case when only one item is selected. Of course this makes more sense when our chosen file is a directory, as we seldom need to tar only one file. Then at the end we give the whole tagged file list directly as arguments to `tar` itself.

Now let's see Numbers 8 and 9, which create Lzip and plain tar archives, respectively.

> + ! t t
> 8       Compress the current subdirectory (tar.lz)
>         Pwd=`basename %d /`
>         echo -n "Name of the compressed file (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar.lz"
>         [ -e "../$ARCHIVE" ] && echo "Error: '../$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         cd .. && \
>             tarlz -cf "$ARCHIVE" "$Pwd" && \
>             echo "../$ARCHIVE created."
>
> + t t
> 8       Compress tagged files (tar.lz)
>         set -- %t
>         CNT=$#
>         case $CNT in
>             1) Pwd=$1;;
>             *) Pwd=`basename %d /`;;
>         esac
>         echo -n "Name of the compressed file (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar.lz"
>         [ -e "$ARCHIVE" ] && echo "Error: '$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         tarlz -cf "$ARCHIVE" %t && \
>             echo "$ARCHIVE created."
>
> + ! t t
> 9       Archive the current subdirectory (tar)
>         Pwd=`basename %d /`
>         echo -n "Name of the archive (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar"
>         [ -e "../$ARCHIVE" ] && echo "Error: '../$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         cd .. && \
>             tar -c --posix -f "$ARCHIVE" "$Pwd" && \
>             echo "../$ARCHIVE created."
>
> + t t
> 9       Archive tagged files (tar)
>         set -- %t
>         CNT=$#
>         case $CNT in
>             1) Pwd=$1;;
>             *) Pwd=`basename %d /`;;
>         esac
>         echo -n "Name of the archive (without extension) [$Pwd]: "
>         read TAR
>         [ "$TAR"x = x ] && TAR="$Pwd"
>         ARCHIVE="$TAR.tar"
>         [ -e "$ARCHIVE" ] && echo "Error: '$ARCHIVE' exists." && \
>             read -p "Press Enter to abort..." dummy && exit 1
>         tar -c --posix -f "$ARCHIVE" %t && \
>             echo "$ARCHIVE created."

Gzip is still the fastest compression method but I am aware that maybe this is its only advantage. It cannot compete in compression ratio (even with `-⁠9`), and lacks the appropriate resistance to corruption that is necessary for long-time archival purposes. I am sure all of the other 5 choices among the default repertoire have their own strengths, but my favorite is Lzip which seem to offer a good compromise in terms of required characteristics. It does offer the parallel implementation `plzip` which could be used directly (as we will see); but here we employ the `tarlz` package that integrate the two, producing an archive where the alignment between tar and lzip members are kept (which is advantageous in case of corruption).

The nice thing in tarlz is that the archives are compatible with GNU tar, and vice versa, posix-format tars can supposedly be turned into proper .tar.lz files (though I have to admit I do not know yet how to check easily for mentioned alignment). For this reason I opted to specify `-⁠-⁠posix` for every tar-generating menu entries here, and anyways I have read somewhere that it was supposed to become the default even in GNU tar not so long ago, but that didn't happen somehow...

And what about the plain tar? For certain critical backups where space does not matter much, it could be more resilient to store the data without compression. For example I have started to use `par2` lately, which is conveniently our next subject, and that is a typical scenario where the file grouping feature is very useful on its own.

Parchive

With this menu item we can create PAR 2.0 recovery files.

> + t r
> p       Parchive current file with par2create
>         par2create -r100 -n2 %p

Parchive was mentioned before in the section about extension config, this is just a handy shortcut to remember using the correct parameters for archival. PAR2's roots can be traced back to Usenet newsgroups, where it has provided a method for correcting corrupted files (that maybe were damaged during transmission) without downloading the whole dataset again. Given that specific use-case, by default without options the par2create program (which is the same as `par2 c`) would produce many files (up to about a dozen, depending on data size), with increasing sizes so given the number of file blocks that failed verification, one could download just the right amount that is required for repair. Also without any parameters it would only contain in total 5% redundancy, which means it would be able to restore a 5% data loss at maximum (although *any* 5%).

Considering purely long-term archival uses we can set redundancy to 100%, so with intact par2 files even a completely missing data file can be recovered. Of course this means we are basically storing the whole data once again with additional overhead, but for important files this is acceptable and provides a more robust arrangement than say just creating a simple copy of the original (in that situation it is not always easy to determine which instance is the corrupted one). Also when we are not operating a network file-share, the number of par2 files can be limited. I have settled for using two so I don't have all my eggs in the same basket, because as far as I can tell corruption in a par2 file can be treated as non-recoverable. Best practice is to verify often to catch any damage early, preferably when it affects only one of the files. In this setup corruption in any of the two files can be repaired, as far as at least half of the original data file is intact; but damage could probably not be fixed when all three suffers the smallest of bit-flips. (Note that I am not counting the fourth small par2 file which is verify-only.)

Extract to subdirectory

The default menu has an item bound to 'x' to extract compressed tar archives into the current directory, and 'z' to extract into a new subdirectory. I decided to merge the two, and as I liked the 'x' variant better I based my version on that.

> #=+ f \.tar\.gz$ | f \.tar\.z$ | f \.tgz$ | f \.tpz$ | f \.tar\.lz$ | f \.tar\.lz4$ | f \.tar\.lzma$ | f \.tar\.lzo$ | f \.tar\.7z$ | f \.tar\.xz$ | f \.tar\.zst | f \.tar\.Z$ | f \.tar\.bz
> #x       Extract the contents of a compressed tar file
> #        unset PRG
> #        case %f in
> #            *.tar.7z)   PRG="7za e -so";;
> #            *.tar.bz2)  PRG="bunzip2 -c";;
> #            *.tar.gz|*.tar.z|*.tgz|*.tpz|*.tar.Z) PRG="gzip -dc";;
> #            *.tar.lz)   PRG="lzip -dc";;
> #            *.tar.lz4)  PRG="lz4 -dc";;
> #            *.tar.lzma) PRG="lzma -dc";;
> #            *.tar.lzo)  PRG="lzop -dc";;
> #            *.tar.xz)   PRG="xz -dc";;
> #            *.tar.zst)  PRG="zstd -dc";;
> #            *)          exit 1;;
> #        esac
> #        $PRG %f | tar xvf -
>
> #+ f \.tar.gz$ | f \.tgz$ | f \.tpz$ | f \.tar.Z$ | f \.tar.z$ | f \.tar.bz2$ | f \.tar.F$ & t r & ! t t
> #z       Extract compressed tar file to subdirectory
> #        unset D
> #        set gzip -cd
> #        case %f in
> #            *.tar.F)   D=`basename %f .tar.F`; set freeze -dc;;
> #            *.tar.Z)   D=`basename %f .tar.Z`;;
> #            *.tar.bz2) D=`basename %f .tar.bz2`; set bunzip2 -c;;
> #            *.tar.gz)  D=`basename %f .tar.gz`;;
> #            *.tar.z)   D=`basename %f .tar.z`;;
> #            *.tgz)     D=`basename %f .tgz`;;
> #            *.tpz)     D=`basename %f .tpz`;;
> #        esac
> #        mkdir "$D"; cd "$D" && ("$1" "$2" ../%f | tar xvf -)
>
> += f \.tar$ | f \.tar\.gz$ | f \.tar\.z$ | f \.tgz$ | f \.tpz$ | f \.tar\.lz4$ | f \.tar\.lzma$ | f \.tar\.lzo$ | f \.tar\.7z$ | f \.tar\.xz$ | f \.tar\.zst | f \.tar\.Z$ | f \.tar\.bz2$ &
> x       Extract (compressed) tar archive
>         unset PRG
>         case %f in
>             *.tar.7z)   PRG="7za e -so";;
>             *.tar.bz2)  PRG="bunzip2 -c";;
>             *.tar.gz|*.tar.z|*.tgz|*.tpz|*.tar.Z) PRG="gzip -dc";;
>             *.tar.lz4)  PRG="lz4 -dc";;
>             *.tar.lzma) PRG="lzma -dc";;
>             *.tar.lzo)  PRG="lzop -dc";;
>             *.tar.xz)   PRG="xz -dc";;
>             *.tar.zst)  PRG="zstd -dc";;
>             *.tar)      PRG="cat";;
>             *)          exit 1;;
>         esac
>         D=`echo "%f" | sed 's/\.tar\.\?[^.]*$//'`
>         echo -n "Name of the uncompressed subdirectory [$D]\n\
>             (set to '.' to extract into the working directory): "
>         read DIR
>         [ "$DIR"x = x ] && DIR="$D"
>         if [ "$DIR" = "." ]; then
>             $PRG %f | tar xvf -
>         else
>             [ -e "$DIR" ] && echo "Error: '$DIR' exists." && \
>                 read -p "Press Enter to abort..." dummy && exit 1
>             mkdir "$DIR" && cd "$DIR" && ($PRG ../%f | tar xvf -)
>         fi

My changes are the following. The extension list changed a bit as I added .tar here as well, to provide the same functionality as with the other compressed formats, also brought out .tar.lz as we will see shortly. Other than these, the rest is basically adding interaction. First we provide a prompt to be able to change the subdirectory name if need be, similar to the way we saw at compression functions before. There is the possibility as well to extract without creating a new directory: I use this seldom as in my opinion it makes a mess more often than not, but anyway I thought why take away a possibly useful feature if I can as well keep it in some form. In case a name was specified (or the default kept) we check if the directory exists already; again this is something that I added in other places before.

> += f \.tar\.lz$ & t rl & ! t t
> x       Extract compressed tarlz archive
>         D=`echo "%f" | sed 's/\.tar\.[^.]*$//'`
>         echo -n "Name of the uncompressed subdirectory [$D]\n\
>             (set to '.' to extract into the working directory): "
>         read DIR
>         [ "$DIR"x = x ] && DIR="$D"
>         if [ "$DIR" = "." ]; then
>             tarlz -xvf %f
>         else
>             [ -e "$DIR" ] && echo "Error: '$DIR' exists." && \
>                 read -p "Press Enter to abort..." dummy && exit 1
>             mkdir "$DIR" && cd "$DIR" && tarlz -xvf ../%f
>         fi
>
> += f \.rar$ & t rl & ! t t
> x       Extract compressed rar archive
>         D=`echo "%f" | sed 's/\.rar$//'`
>         echo -n "Name of the uncompressed subdirectory [$D]\n\
>             (set to '.' to extract into the working directory): "
>         read DIR
>         [ "$DIR"x = x ] && DIR="$D"
>         if [ "$DIR" = "." ]; then
>             unrar -x %f
>         else
>             [ -e "$DIR" ] && echo "Error: '$DIR' exists." && \
>                 read -p "Press Enter to abort..." dummy && exit 1
>             mkdir "$DIR" && cd "$DIR" && unrar -x ../%f
>         fi

Next we have a separate function for .tar.lz files, to be able to use the `tarlz` utility, but otherwise the commands are exactly the same. Similarly I have included one for RAR as well, mainly as a small consolation for my earlier failures in getting the virtual filesystem working. Again I only had to change the extractor invocation here.

Finally, let's see the tagged variant. I have included 'Z' here from the original just as reference only, but the new one is of course based on 'x'.

> #+ t t
> #Z       Extract compressed tar files to subdirectories
> #        for i in %t ; do
> #            set gzip -dc
> #            unset D
> #            case "$i" in
> #                *.tar.F)   D=`basename "$i" .tar.F`; set freeze -dc;;
> #                *.tar.Z)   D=`basename "$i" .tar.Z`;;
> #                *.tar.bz2) D=`basename "$i" .tar.bz2`; set bunzip2 -c;;
> #                *.tar.gz)  D=`basename "$i" .tar.gz`;;
> #                *.tar.z)   D=`basename "$i" .tar.z`;;
> #                *.tgz)     D=`basename "$i" .tgz`;;
> #                *.tpz)     D=`basename "$i" .tpz`;;
> #          esac
> #          mkdir "$D"; (cd "$D" && "$1" "$2" "../$i" | tar xvf -)
> #        done
>
> + t t
> X       Extract tagged (compressed) tars to subdirectories
>         for i in %t ; do
>             unset PRG
>             case "$i" in
>                 *.tar.7z)   PRG="7za e -so";;
>                 *.tar.bz2)  PRG="bunzip2 -c";;
>                 *.tar.gz|*.tar.z|*.tgz|*.tpz|*.tar.Z) PRG="gzip -dc";;
>                 *.tar.lz)   PRG="lzip -dc";;
>                 *.tar.lz4)  PRG="lz4 -dc";;
>                 *.tar.lzma) PRG="lzma -dc";;
>                 *.tar.lzo)  PRG="lzop -dc";;
>                 *.tar.xz)   PRG="xz -dc";;
>                 *.tar.zst)  PRG="zstd -dc";;
>                 *.tar)      PRG="cat";;
>                 *)          echo "Warning: '$i' skipped"; continue;;
>             esac
>             D=`echo "$i" | sed 's/\.tar\.\?[^.]*$//'`
>             echo "Extracting: '$i' to '$D'"
>             [ -e "$D" ] && echo "Error: '$D' exists." && \
>                 read -p "Press Enter to abort..." dummy && exit 1
>             mkdir "$D" && cd "$D" && ($PRG ../$i | tar xvf -) && cd .. && continue
>             exit 1
>         done

A couple of things to note. Lzip is included just as in the original, handled through `tar` the same way as the other formats, so no `tarlz` here (but at this point I am not entirely sure that it would make any difference for extraction). There are no prompts to change the new subdirectories or extract any of the archives in the working directory, this would overly complicate matters I thought. Also we check for existence of the dir but this is short-circuiting, meaning that the whole function exits on the first such error, leaving some items extracted some without. This should probably be improved, but honestly I do not feel the mental strength right now to try coming up with a better solution (especially as it has to be considered that either *all* or *none* of the tagged files can remain to be tagged afterwards).

Plzip files

With this we round off compression-related entries in the menu. The original source has functions to call gzip or bzip2 on a file or set of tagged files directly, to compress or extract them in-place. These are pretty useful so I just copied them to use the Lzip program (parallel implementation).

> + ! t t
> l       Plzip or decompress current file
>         unset DECOMP
>         case %f in
>             *.lz) DECOMP=-d;;
>         esac
>         plzip $DECOMP -v %f
>
> + t t
> L       Plzip or decompress tagged files
>         for i in %t ; do
>             unset DECOMP
>             case "$i" in
>                 *.lz) DECOMP=-d;;
>             esac
>             plzip $DECOMP -v "$i"
>         done

Diff

The dedicated `mcdiff` program can be accessed under Command menu -⁠> "Compare files" (shortcut Ctrl-⁠X Ctrl-⁠D), but that only works using both the left and right panes. Many times this is not convenient, for example when we have a backup and a newer file right beside each other. We would have to Alt-⁠I to match the working directory of the two panes, Tab over and move the pointer, then afterwards use the Alt-⁠H directory history to get back where we were before.

This new menu entry makes diff work on two or three tagged files. Unfortunately, `mcdiff` does not support three-way comparison so I had to resort to `diff3` for that.

> + t t
> d       Diff tagged files
>         set -- %t
>         CNT=$#
>         case $CNT in
>             2) mcdiff $1 $2;;
>             3) echo "1=`realpath $1`"
>                 echo "2=`realpath $2`"
>                 echo "3=`realpath $3`"
>                 echo -n "File order for 3-way diff (MYFILE OLDFILE YOURFILE) [123]: "
>                 read ORDER
>                 [ "$ORDER"x = x ] && ORDER="123"
>                 INVALID=false
>                 MATCH=`expr match $ORDER "^[123]\{3\}\$"`
>                 if [ $MATCH != 0 ]; then
>                     N1=`expr substr $ORDER 1 1`
>                     N2=`expr substr $ORDER 2 1`
>                     N3=`expr substr $ORDER 3 1`
>                     eval "P1=\${$N1}"
>                     eval "P2=\${$N2}"
>                     eval "P3=\${$N3}"
>                     if [ $P1 != $P2 ] && [ $P1 != $P3 ] && [ $P2 != $P3 ]; then
>                         { echo "1=`realpath $P1`"
>                             echo "2=`realpath $P2`"
>                             echo "3=`realpath $P3`\n"
>                             diff3 $P1 $P2 $P3
>                         } | pager
>                     else
>                         INVALID=true
>                     fi
>                 else
>                     INVALID=true
>                 fi
>                 if $INVALID; then
>                     echo "Error: invalid order specified."
>                     read -p "Press Enter to abort..." dummy
>                 fi
>                 ;;
>             *) echo "Error: invalid number of tagged items."
>                 read -p "Press Enter to abort..." dummy;;
>         esac

Excuse all the mess at case 3, I will explain: it is a method to let the user select the ordering of the three input files. To be honest, it is not that critical for `diff3` specifically, but maybe I will find some better alternative one day (that can display side-by-side), and I definitely needed this for `meld` (as we will see in a minute), so I thought I might just as well include it in here too. You see in three-way comparisons generally there is one that should be, or rather *wants to be* in center position - again, in case the program has the ability to display it so. Even in `diff3` where positions are indicated only with numbers on output, the arguments are labeled "MYFILE OLDFILE YOURFILE" in the manual, so who knows, this might be useful to someone even like this.

The ordering code works the following way. First we state the file paths as we got them through the tag list, assigning indexes 1-⁠3 to them. Then we ask the user to put this three number characters in the order they would like them to be forwarded to the diff program. On empty input the default is "123". Then we check with a regexp match, that only these three characters were used, exactly three times. After this we extract the individual characters, and use them as indexes to our file array (note in POSIX shell we need `eval` for this). We only have to take care one last problem at this point, which we could not filter with regexp: if the same character was used multiple times. At last we print the final indexes and file paths again, but this time this is going to a pager, along with the `diff3` call using the desired argument ordering.

Meld

I am preaching on this blog the importance of getting to know text-based development tools, and diff/diff3/mcdiff are fine examples for that; but I am not disputing the fact that certain tasks can be done in a more productive way using a GUI. My go-to graphical comparison tool is `meld`, so naturally I have created the related user menu items as well.

> + t r & T r & ! t t
> m       Meld current files
>         meld %d/%p %D/%P &
>
> + t t
> m       Meld tagged files
>         set -- %t
>         CNT=$#
>         case $CNT in
>             2) meld $1 $2 &;;
>             3) echo "1=`realpath $1`"
>                 echo "2=`realpath $2`"
>                 echo "3=`realpath $3`"
>                 echo -n "File order for 3-way meld (MYFILE OLDFILE YOURFILE) [123]: "
>                 read ORDER
>                 [ "$ORDER"x = x ] && ORDER="123"
>                 INVALID=false
>                 MATCH=`expr match $ORDER "^[123]\{3\}\$"`
>                 if [ $MATCH != 0 ]; then
>                     N1=`expr substr $ORDER 1 1`
>                     N2=`expr substr $ORDER 2 1`
>                     N3=`expr substr $ORDER 3 1`
>                     eval "P1=\${$N1}"
>                     eval "P2=\${$N2}"
>                     eval "P3=\${$N3}"
>                     if [ $P1 != $P2 ] && [ $P1 != $P3 ] && [ $P2 != $P3 ]; then
>                         meld $P1 $P2 $P3 &
>                     else
>                         INVALID=true
>                     fi
>                 else
>                     INVALID=true
>                 fi
>                 if $INVALID; then
>                     echo "Error: invalid order specified."
>                     read -p "Press Enter to abort..." dummy
>                 fi
>                 ;;
>             *) echo "Error: invalid number of tagged items."
>                 read -p "Press Enter to abort..." dummy;;
>         esac

After all the previous discussions, I think the only new element here might be that we can use the current directory and file of the *other* pane as well, using upper-case macros `%D` and `%P` etc. The tagged entry works almost the same as at the diffs, just with the different finishing command. Meld supports three-way comparisons, but in this mode it is not possible to change the file ordering at runtime, strangely; thus hopefully you can see how important it was to get this little interaction at startup right.

The program also supports recursively comparing two directories:

> M       Meld current directories
>         meld %d %D &

I admit this was a feature I did not know about for a long time, and was glad to find it. I have used the synchronization feature in Total Commander quite often, and at times things like these can help greatly in understanding certain file system situations. Now I just wish I also had something similar that is text-based... Spoiler alert: I might have accidentally started thinking about and working on a solution for this...

Meld is also great to look at version control changes:

> G       Show Git changes in working directory (Meld)
>         WORKTREE=`git rev-parse --is-inside-work-tree 2>/dev/null`
>         STATUS=$?
>         if [ "$WORKTREE" != "true" ]; then
>             if [ $STATUS = 0 ]; then
>                 echo "Warning: inside .git directory."
>             else
>                 echo "Warning: directory not under version control."
>             fi
>             read -p "Do you want to proceed? [y] " RES
>             [ "$RES"x = x ] && RES="y"
>             case $RES in
>                 y | yes | Y | Yes | YES) ;;
>                 *) echo "Aborting..."
>                     exit 1;;
>             esac
>         fi
>         meld . &

This time I am starting with the variant for directories, as this is the simpler case. The last line is the most important here of course, but it does not make much sense to call it on something that is not under version control. So all the preceding code is just to check if we are in a Git repo, using the `-⁠-⁠is-⁠inside-⁠work-⁠tree` call that can distinguish between a work tree and the `.git/⁠` directory, or returns an error status otherwise.

The user can still proceed even if our simple "heuristic" fails, for example if we are under a different VCS. Note that Meld supports about half a dozen other systems as well, so my method could be extended accordingly (and probably the hotkey changed to something more general e.g. 'V'). Similarly, this could be easily adapted to use `git diff` or `git status`, to have a text-based alternative too. I opted against that now, as I felt then why not include a couple of other common Git commands as well in the menu, but the line had to be drawn somewhere.

> g       Show Git changes in current file (Meld)
>         if [ -d %p ]; then
>             WORKTREE=`cd %p; git rev-parse --is-inside-work-tree 2>/dev/null`
>         else
>             WORKTREE=`git rev-parse --is-inside-work-tree 2>/dev/null`
>         fi
>         STATUS=$?
>         if [ "$WORKTREE" != "true" ]; then
>             if [ $STATUS = 0 ]; then
>                 echo "Warning: inside .git directory."
>             else
>                 echo "Warning: directory not under version control."
>             fi
>             read -p "Do you want to proceed? [y] " RES
>             [ "$RES"x = x ] && RES="y"
>             case $RES in
>                 y | yes | Y | Yes | YES) ;;
>                 *) echo "Aborting..."
>                     exit 1;;
>             esac
>         fi
>         meld %p &

The entry works almost the same as on files, except if we are pointing at a directory, we would like to know if *that* directory itself is part of a repo, instead of the current one. This is a similar situation to others we saw before, where invoking a function on a directory - as a file - does the same as going into it and calling the related "do something on this working directory" item. It is indeed redundant work but when we are talking about increasing usage comfort, even the small things can matter greatly.

Putting it all together

I would like to address some of the considerations regarding other changes I had to make, to fit the new entries. By "fitting" I am referring to the fact that even though not all options are displayed for every file type, if there are too many, it may not show up well on a 24-⁠line screen. I mean, okay sometimes I am using even less, but that seems like a decent goal. With the default config, there are 21 options for a compressed tar archive, which is I think the case with the most items available. I have found that a 21-⁠item user menu can only be displayed fully in a 27 tall screen at minimum, without a scrollbar, which I would like to avoid. So this is meaning I cannot have more than 18 entries if I would like it to behave well in the default 80x24 window.

In order to keep the number of entries under the determined target, I had to disable some of the default items, but they have been retained in comment blocks. Maybe now that the file ended up in version control, they could be cleaned up but I fear I would forget about them completely.

Please excuse that I advance in logical order rather than by file line positions. I must admit the "Convert gz<‑>bz2, tar.gz<‑>tar.bz2" items (shortcut 'c' and 'C') are kind of neat, but as I stopped using bzip2, these lost their utility. I could have replaced it with an Lzip version, maybe I will do it someday but the operation is not that frequent for me really, to justify the effort right now.

Staying at the letter 'c', there was also "Compile and link current .c file" and somewhat similarly, "Run latex on file and show it with xdvi". Both systems I used to use at certain points in my life, but not anymore or at least not that often, that I would like to reserve dedicated spots for them. I do not even know if these would work with files part of larger projects, would have to experiment, nonetheless I still have the option to activate them anytime I change my mind - you see that is why I hesitate to throw away this kind of stuff.

After things that I could use yet, let's see things that I could use but do not trust. Do not take this the wrong way, they probably work just fine, but usually I like to be absolute sure that no mistakes are made when it comes to these kinds of operations. Take for example "Delete file if a copy exists in the other directory": what could possibly go wrong?! Sure this might come handy when someone has to do this on hundreds of files; but until then it remains disabled for me. The other one is "Copy file to remote host" that basically just calls `rcp`; I like to do this via the "Shell link..." feature of MC, where I am actually seeing what I am doing, but sure it might not be as productive if had to do it very often. Maybe there are situations where RCP would work and the "link" not, I am not a networking expert, but I had no trouble so far. Nonetheless, the label on the tagged variant reading "(no error checking)" does not instill much confidence, to be honest.

The next batch of entries would all invoke programs that I do not usually have installed, on stock Debian. Items "Display the file with roff" and "Call the info hypertext browser" are self-explanatory. Then "Open next a free console" would want to use `open` with an option that crashes `xdg-⁠open` (to which my `open` seem to be linked). Obviously the original `open` must have worked in a different way, but anyway the intent was to start a new POSIX shell. Right now I cannot see what utility this would serve inside MC, so I will have to research further.

I apologize for my ignorance, but it seems to me that several entries here - including the last one I have mentioned - are left from a time that was a bit different to the present. One of the first items in the original list, keyed to Number 0 is "Edit a bug report and send it to root". Okay I understand how this works on a multi-user system, which in itself can still be a thing at certain (big) places; but honestly, when was the last time you actually relied on getting important feedback through the root mailbox like this?! Then there are no less than four items concerning files associated with Usenet newsgroups, doing tasks like decoding, inspecting, and stripping. Maybe it is not surprising that I have never used the service (or actually seen it in operation), as the WWW was well underway when I started to understand computers. I find it sad that I doubt I will ever get to know how it works, as even some of those forms of electronic social interactions I grew up with - thinking of forums in particular - vanish with the emergence of the newfangled social media platforms. In any case, I am disabling these in the menu - for now.

Conclusion

Again, the complete `menu` file can be found up top via the GitHub link. Remember that really my modifications are just small improvements to the original. I would like to express my appreciation to the creators of GNU Midnight Commander, especially for taking the time to make an already awesome program user-extendable.

Thank you for joining! I hope you can use one or two of the new User menu entries, or at least you got interested in doing your own customizations.