How do I make a local HTML page auto-refresh on file change?

Solution 1:

A More General Solution

Javascript alone does not seem to be able to solve this problem. Until browsers add back in the support they used to have for doing this, I don't think there's a perfectly general solution.

While I think my previous Emacs solution is a good one, for people who use text editors that do not have builtin web servers, here's another answer which is a bit broader.

Use inotifywait

Many OSes can setup a program to execute whenever a file is modified without having to poll. There is no one API for all OSes, but Linux's inotify works better than most and is easy to use.

Here is a shell script which, when run in the directory where your HTML and CSS files are, will tell Firefox to reload whenever changes are saved. You could also call it with specific filenames if you want it to only watch a few files.

#!/bin/bash
# htmlreload
# When an HTML or CSS file changes, reload any visible browser windows.
# Usage:
# 
#     htmlreload [ --browsername ] [ files ... ]
#
# If no files to watch are specified, all files (recursively) in the
# current working directory are monitored. (Note: this can take a long
# time to initially setup if you have a lot of files).
#
# An argument that begins with a dash is the browser to control.
# `htmlreload --chrom` will match both Chromium and Chrome.

set -o errexit
set -o nounset

browser="firefox"      # Default browser name. (Technically "X11 Class")
keystroke="CTRL+F5"    # The key that tells the browser to reload.

sendkey() {
    # Given an application name and a keystroke,
    # type the key in all windows owned by that application.
    xdotool search --all --onlyvisible --class "$1" \
        key --window %@ "$2"
}

# You may specify the browser name after one or more dashes (e.g., --chromium)
if [[ "${1:-}" == -* ]]; then
    browser="${1##*-}"
    shift
fi

# If no filenames given to watch, watch current working directory.
if [[ $# -eq 0 ]]; then
    echo "Watching all files under `pwd`"
    set - --recursive "`pwd`" #Added quotes for whitespace in path
fi

inotifywait --monitor --event CLOSE_WRITE "$@" | while read; do
    #echo "$REPLY"
    sendkey $browser $keystroke
done

Prerequisites: inotifywait and xdotool

You'll need inotifywait and xdotool installed for this to work. On Debian GNU/Linux (and descendants, such as Ubuntu and Mint) you can get those programs using a single command:

sudo apt install inotify-tools xdotool

Optional: Working with Chromium

I suggest using Firefox due to a strangeness in the way Chromium (and Chrome) handle input in windows that do not have focus. If you absolutely must use Chromium, you can use this sendkey() routine instead:

sendkeywithfocus() {
    # Given an application name and a keystroke, give each window
    # focus and type the key in all windows owned by that application.

    # This is apparently needed by chromium, but is annoying because
    # whatever you're typing in your text editor shortly after saving
    # will also go to the chromium window. 

    # Save previous window id so we can restore focus.
    local current_focus="$(xdotool getwindowfocus)"

    # For each visible window, focus it and send the keystroke.
    xdotool search --all --onlyvisible --class "$1" \
        windowfocus \
        key --window %@ "$2"

    # Restore previous focus.
    xdotool windowfocus "$current_focus" 
}

Optional: Working in Wayland

I have not tested it out, but read that Wayland now has a program called ydotool which is a drop in replacement for xdotool.

Solution 2:

Emacs Impatient Mode

In one of the comments, the questioner mentioned that they use the Emacs text editor. Emacs has a simple solution for live updating HTML (and CSS) as you type: Impatient Mode.

It uses an Emacs web server to serve a page with Javascript that shows live updates on each keystroke.

Installation

If you have setup MELPA, Impatient Mode can be easily installed with

M-x package-install

Alternately, if you prefer to install by hand, see my instructions below.

Using impatient-mode

There are just three steps:

  1. Run once:

     M-x httpd-start
    
  2. Run in any HTML or CSS buffer you're editing:

     M-x impatient-mode
    
  3. Open your browser to http://localhost:8080/imp and click on the name of the buffer.

Now, just type in Emacs and watch the magic happen!

Usage Side Note

I've submitted a patch to the maintainer of Impatient Mode that automatically starts the web server and opens up the proper URL in your browser when you run M-x impatient-mode. Hopefully that will be accepted and you'll only need one step to do everything. I will edit this answer if that occurs.


Optional: Installing by hand

The following is not necessary, but some people would prefer to not add MELPA to their Emacs package repository list. If that is the case, you can install Impatient Mode like so:

cd ~/.emacs.d
mkdir lisp
cd lisp
git clone https://github.com/skeeto/impatient-mode
git clone https://github.com/skeeto/simple-httpd
git clone https://github.com/hniksic/emacs-htmlize/

Now edit your .emacs file so it adds subdirs of ~/.emacs.d/lisp/ to the load-path:

;; Add subdirectories of my lisp directory. 
(let ((default-directory  "~/.emacs.d/lisp"))
     (normal-top-level-add-subdirs-to-load-path))
(autoload 'impatient-mode "~/.emacs.d/lisp/impatient-mode/impatient-mode" "\
Serves the buffer live over HTTP.

\(fn &optional ARG)" t nil)

That should be enough for Impatient Mode to work, but if you'd like it to be slightly faster, you can byte-compile the emacs lisp files.