Support Your Local Viewer

Posted in offlining bash markdown pandoc vimb w3m lynx xdg

Markdown is the fast-food of document formats.

That doesn't change the fact that it's everywhere.

So much everywhere, in fact, that it's kind of puzzling there is not a dedicated tool around to view it.

Pinning down markdown

There is no shortage of applications that can render markdown. Among the alternatives are free code editors like Atom or Geany, the browser plugin Markdown Viewer and even a dedicated markdown editor like Marktext, retext and ghostwriter.

And of course, there exist SaaS offerings such as hackmd. But seeing as those are not alternatives for offline use, we don't concern ourselves with those here.

But what is the equivalent of sxiv or feh for Markdown?

Honestly, I couldn't find any such thing. If there is, I'd love to know.

Fortunately, there is a perfectly reasonable workaround.

Step by step

After all, it is in the spirit of *nixes to use a choice of tools who does one thing and does it well.

So, no matter how bizarre it feels, it may make sense that a lurid format like Markdown should be treated in a separate step to produce a more well-established - and less ambiguous - format.

I asked a related question on Stackexchange long ago, and there the pandoc tool came up as a solution.

And it turns out it works beautifully in this case aswell.

Consider the following script:

t=$(mktemp --suffix=.html)
2>&1 echo $t
pandoc -f gfm -t html -M document-css=false --standalone $1 > $t 2> /dev/null
w3m $t

Quite simply, we generate a standalone html file in tmpfs, which in turn is read and renderered by a web browser.

Browsing browsers

No appreciation for w3m, eh? Instead want that fuzzy feel of comforting colors, smooth scroll and fancy fonts?

I can understand. I used to suffer that affliction, too.

But nonetheless; it's an important point. For example, what if I wanted to use lynx or vimb instead? Choosing the browser to view with should definitely be the caller's call.

Is there a canonical way of doing that in Linux.

Kind of.

Let's review a couple of the options.

The environmental solution

Some applications honor the $BROWSER environment variable. So let's cover for that:

browser=${BROWSER:-w3m}
t=$(mktemp --suffix=.html)
pandoc -f gfm -t html -M document-css=false --standalone $1 > $t 2> /dev/null
$browser $t

Now, viewing the markdown file README.md with lynx is as easy as:

$ BROWSER=$(which lynx) bash wmd.sh README.md

The cross solution

So, have you heard about the Cross Desktop Group? [1] These are the guys you should be sending a thought of gratitude every time you intuitively look in your ~/.config or ~/.local folder for application data. And if you're a HUIf [2] coder, chances are you have seen the string xdg in some function call somewhere.

These days they go by the name Free Desktop Group, and among other things they have negotiated something particularly useful to us in this particular case.

To launch programs in a desktop environment in Linux, a Desktop Entry Specification file format [3] has been defined - appropriately suffixed .desktop.

Take a look at a random *.desktop file in /usr/share/applications (your most likely default $XDG_DATA_DIRS location, where xdg searches for desktop files). Every one will contain an top-level Exec= entry.

For example, my feh.desktop entry has Exec=feh --start-at %u which means find feh in $PATH and execute it with the --start-at switch and one single url as argument:

[Desktop Entry]
Name=Feh
Name[en_US]=feh
GenericName=Image viewer
GenericName[en_US]=Image viewer
Comment=Image viewer and cataloguer
Exec=feh --start-at %u
Terminal=false
Type=Application
Icon=feh
Categories=Graphics;2DGraphics;Viewer;
MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/tiff;image/webp;image/x-bmp;image/x-pcx;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-tga;image/x-xbitmap;image/heic;
NoDisplay=true

Covering this case in our script:

fallbackbrowsercmd=w3m
browsercmd=
# if browser env exists, then
# try handle it as a desktop entry
if [ ! -z "$BROWSER" ]; then
        # find the xdg paths
        XDG_DATA_DIRS=${XDG_DATA_DIRS:-/usr/share}
        if [ ! -z "$XDG_DATA_HOME" ]; then
                XDG_DATA_DIRS=$XDG_DATA_HOME:$XDG_DATA_DIRS
        fi
        # split on ":" and try dirs one by one
        # use first matching browser desktop entry
        _ifs=$IFS
        IFS=:
        dirs=("$XDG_DATA_DIRS")
        for d in $dirs; do
                s=$BROWSER.desktop
                a=$d/applications/$BROWSER.desktop
                if [ -f "$a" ]; then
                        browsercmd="gtk-launch $s"
                fi
        done
        IFS=
fi
# if no browser set or could not be found in xdg,
# then try the browser env var as command, or
# ultimately the static fallback
if [ -z "$browsercmd" ]; then
        browsercmd=${BROWSER:-$fallbackbrowsercmd}
fi
t=$(mktemp --suffix=.html)
pandoc -f gfm -t html -M document-css=false --standalone $1 > $t 2> /dev/null
$browsercmd $t

The default solution

Yes, there is such a thing as "default web browser" in XDG, too.

Have a look at xdg-settings --list. On my system, it shows a rather modest output:

$ xdg-settings --list
Known properties:
        default-url-scheme-handler    Default handler for URL scheme
        default-web-browser           Default web browser

And the default web browser is:

$ xdg-settings get default-web-browser
brave-browser.desktop

Yeah, yeah, yeah. Busted. I use graphical browsers, too.

Now let's add this to the mix, then.

fallbackbrowsercmd=w3m
browsercmd=
# if browser env var not set, set it with default browser
if [ -z "$BROWSER" ]; then
        BROWSER=$(xdg-settings get default-web-browser)
        BROWSER=${BROWSER%%.*}
fi
if [ ! -z "$BROWSER" ]; then
        XDG_DATA_DIRS=${XDG_DATA_DIRS:-/usr/share}
        if [ ! -z "$XDG_DATA_HOME" ]; then
                XDG_DATA_DIRS=$XDG_DATA_HOME:$XDG_DATA_DIRS
        fi
        _ifs=$IFS
        IFS=:
        dirs=("$XDG_DATA_DIRS")
        for d in $dirs; do
                s=$BROWSER.desktop
                a=$d/applications/$BROWSER.desktop
                if [ -f "$a" ]; then
                        browsercmd="gtk-launch $s"
                fi
        done
        IFS=
fi
if [ -z "$browsercmd" ]; then
        browsercmd=${BROWSER:-$fallbackbrowsercmd}
fi
t=$(mktemp --suffix=.html)
pandoc -f gfm -t html -M document-css=false --standalone $1 > $t 2> /dev/null
$browsercmd $t

Naming the executioner

So now we have a markdown viewer. And it can be as lean or as heavy as you want it to be. It's all to the browser you choose.

To make it accessible, map the script as a command alias in your .profile or .bashrc and you have the viewer at your fingertips.

I call mine wmd:

alias wmd="bash $HOME/scripts/markdown.sh"

And voilá:

$ export BROWSER=lynx
$ wmd README.md
[1]Or X Desktop Group, as they were originally called.
[2]Human User Interface. Don't bother looking - I made it up. As the cause of AI, robots and machines inevitably will become appropriated by the woke intersectionality complex, the terminology is probably going to need such distictions.
[3]Actually the format is ini. The standard is rather in the file naming, really.