Most solutions to automatic browser refresh for development are silly:

  • Browser extensions for automatic reloading every n seconds (like Auto Reload Tab or Tab Reloader) cause lots of unnecessary load in client and server, and can’t react directly to file changes.
  • Some browser extensions (like Live Reload) employ some sort of polling against chosen remote files to decide if they should reload the entire tab. This lowers the load somewhat, but it still doesn’t react directly to file changes.
  • The browser extension FileWatcher used to be the perfect tool for the job, but it hasn’t worked ever since Mozilla killed proper Firefox extensions, and currently doesn’t appear to run on Linux at all, being developed exclusive on Windows.
  • Live.js, which is very popular on StackExchange, is JavaScript code to include in your opened webpage. Like Live Reload, it polls for changes and reloads the site. By default, it loads remotely from https://livejs.com (which seems crazy to me when you’re developing locally). You can also host the code locally, but there’s still the same problems as with the browser extensions. Also, Live.js employs a decreasing polling frequency, so if you turn away from developing and come back a while later, your changes may take minutes to show up in the browser. Also, Live.js has frustrated users in local development by complaining: Live.js doesn't support the file protocol. It needs http.

Here, I describe a way to solve all of these problems and refresh your browser whenever any local file changes. It is guaranteed to work with any of the five browsers currently in existence: Firefox, Chrome, Chrome in a hat, Chrome with a moustache and Chrome in Bermuda shorts.

You’ll need:

  • Linux with inotify-tools installed (should be available in every distribution)
  • an X server (sorry, Wayland can’t do this) and xdotool installed
  • any web server, which can be running:
    • locally on the machine where you’re editing files, like this script using Python 3’s http.server module
    • remotely on another machine, as long as you’re editing the hosted files through a local mount – I’ve successfully tested this with NFS and sshfs

The basic idea is a bash while loop in which inotifywait continually waits for one or more files to be locally modified, which it does by hooking itself to Linux kernel calls, requiring no polling whatsoever. When such a change happens, xdotool is called to find a window with a specific title, activate that window and send it an F5 keypress, and finally activate whichever window was active before. (This may sound like a very roundabout way to send a refresh command to a browser, but I’ve found that Firefox is thoroughly nailed shut and can’t be controlled from the console worth a damn.)

Here’s my script serverefresh.bash:

#!/bin/bash

trap 'kill $PYID; exit' INT      # kill the server when script is CTRL+C'd
python3 -m http.server &         # run the server
PYID=$!                          # record its PID (for killing)

                                 # main loop runs once whenever the files
                                 # listed here change -- modify as needed
while inotifywait -qq -e attrib *.html *.css; do
                                 # you might have to replace attrib in the
                                 # above line with move_self or some other
                                 # event -- see blogpost
 OWID=$(xdotool getwindowfocus)  # record which window is active right now
 RWID=$(xdotool search --name "$(<serverefresh.title)" | head -1)
                                 # the text file serverefresh.title contains
                                 # the title of the browser window to be
                                 # refreshed -- can be changed on the fly
 if [ -z "$RWID" ]; then
  echo "No such window!"         # warn if no window with that title found
  continue
 fi
 xdotool windowactivate $RWID    # activate (=click) window with title
 xdotool key F5                  # send F5 keypress
 xdotool windowactivate $OWID    # re-activate previously active window
done

A couple of notes:

  • inotifywait is called with -e attrib here, but you may need to specify a different event. It appears to depend on the editor you’re using: with KDE’s editors, ATTRIB is the first change that inotifywait registers when saving a file. With vim, I’ve found it to be MOVE_SELF. Beware that inotifywait -qq may silently fail with return code 1 if something entirely different from the specified event happens to the file first. You can test which events are fired by using a small loop that watches a file you are handling. You run the following command:
    • $ while inotifywait testfile; do true; done
  • … watch its output and play around with the file by opening, editing and saving it in your editor. You’ll hopefully see which event is the right one to feed to inotifywait -e <event>. Don’t try to be clever and drop -e completely, or you’ll run into a comical infinite loop, in which the OPEN event triggers the loop, which refreshes the browser, which causes the web server to OPEN the file, … you get the picture.
  • In my script, the files to be watched are hardcoded as *.html *.css. You can obviously modify this, but you might also specify them in a file, similar to how serverefresh.title is handled. Or you might take them from the command line arguments to this script, in which case the while line could look like this:
    • while inotifywait -qq -e <event> $@; do
  • When xdotool windowactivate activates a window, it is un-minimised and raised above other windows, as if you had clicked it. If this bothers you, try to set other windows to be Always on top, or a similar function of your window manager.
  • Sometimes, the xdotool search apparently returns a bogus window ID that doesn’t work. I’ve managed to circumvent this problem by being as specific as possible with the window title. Also, remember that most browsers display the content of the <title> tag in the window title of the application, so you might include a unique string there to reliably guide xdotool to its target.

Happy coding!

getmail is a fetchmail replacement, and it is a wonderful piece of software that has never failed me in 15 years.

With the sunsetting of Python 2 looming in 2019, the original author Charles Cazabon was in no hurry to publish a Python 3 replacement, but at the same time said he was working on a rewrite for Python 3.7, albeit with an unclear timeline. As distributions began removing getmail along with Python 2 in 2020, Roland Puntaier stepped in to fill the gap with a fork called getmail6, compatible with both Python 2.7 and 3.x. So far, it’s been picked up by at least Debian, ArchLinux and FreeBSD Ports. Charles did a phenomenal job with getmail, so I hope to see his rewrite someday, but for the moment I’m glad that Roland created getmail6 as a drop-in replacement.

Anyway, all this commotion around getmail has inspired me to investigate an old annoyance popping up in my mail system logs, which is the following warning by dovecot-lda:

Warning: Failed to parse return-path header

Googling yields almost no meaningful results, so this appears to be a very rare phenomenon. I guess my use-case is rather esoteric, or people don’t usually care about this warning. For me, anyway, it was very common: roughly 30–50% of all incoming messages generated this warning.

As it turns out, this is because of a misunderstanding between getmail and dovecot-lda. Being a very dutiful MTA, getmail always adds a Return-Path header to every message it processes. However, if the incoming message never had a Return-Path header to begin with, getmail is facing a dilemma: It doesn’t have any value to put in Return-Path, but being a faithful MTA, it must set something. So, as a compromise, getmail writes:

Return-Path: <unknown>

For most users, getmail would then deposit the message in a Maildir or an mbox file, and nobody would ever care about the Return-Path header.

But in my case, I want to filter the incoming messages with Sieve before saving them in my Maildir, so I use getmail’s MDA_external destination to pass the message to dovecot-lda, which handles filtering and final devliery. Upon parsing an incoming message, if dovecot-lda sees Return-Path: <unknown>, it (understandably) spouts the abovementioned warning.

Now, the whole point of the Return-Path header is to always have an answer to the question: „If something goes wrong right now, where do we send the bounce message?“ In my case, this is irrelevant, because once getmail has downloaded a message from the server that was last holding it, it will only be processed on my machine, and there will never be a reason for me to send bounces. With dovecot-lda, this behavior is represented by the -e flag: If a message fails to deliver locally, it spouts an error and bails without trying to send a bounce. However, even with this -e flag set, it will try to parse the Return-Path header, and give the abovementioned warning if getmail has set it to <unknown>.

In order to silence this warning, I’m employing a very ugly hack, which is to call dovecot-lda as:

dovecot-lda -e -f invalid@invalid.invalid

… or, in getmail configuration:

[destination]
type = MDA_external
path = /usr/local/libexec/dovecot/dovecot-lda
arguments = ('-e', '-f', 'invalid@invalid.invalid')

This forces dovecot-lda to use invalid@invalid.invalid as the Return-Path, which is a syntactically valid address, although it obviously leads nowhere. (Then again, with ICANN going nuts on TLDs, who even knows.) In any case, it silences the warning, because dovecot-lda now ignores getmail’s Return-Path: <unknown>, and happily works with invalid@invalid.invalid as the Return-Path (which, because of the -e flag, it will never send any message to anyways).

It there a more sane way to handle this? Not without either getmail or dovecot-lda changing their default bevahior or configuration options. I’ve discussed this at length in this Github issue, but for now, I’m just happy with the warning gone from my mail logs.

I recently ran into this bug (or maybe just „weird, undocumented behavior“) of mc(1) when setting it up on FreeBSD. Even after saving the „Verbose operation“ option in the „Configuration“ menu, it was always disabled after the next startup of mc. I could confirm that verbose=true got set in the $HOME/.config/mc/ini file (probably $HOME/.mc/ini on Linux), but was ignored (or reset) on startup.

Now, „Verbose operation“ makes all those nice progress bar windows pop up, and without it there’s no point in using mc! So what’s going on here? Apparently, mc checks your terminal’s reaction speed in terms of „baud“, like it’s 1997 and we’re running stuff on serial cables or something. If mc finds that your terminal is too slow, you don’t get any progress bars (awww), because your terminal apparently can’t handle it.

Now, under some circumstances, some terminals don’t report a proper baud speed – maybe because it’s not 1997, and we’re not running stuff on serial cables. In these cases, the function tty_baudrate() returns -1, which I guess should alert programs that the value is useless. But Midnight Commanders just asserts that this value is smaller than 9600 (duh!) and always sets verbose=FALSE; on startup, because your serial cable is too thin. Gotcha!

The bug has been known and ignored for 6 years (mc Trac ticket #2452), but I can confirm that the patch proposed in comment #15 does the trick. I’ve submitted it to the maintainer of the mc port in FreeBSD, and maybe you want to go and bug mc people to fix it or get your distro to incorporate the patch – because who on earth would want to to use Midnight Commander without those progress bars, am I right?!