Auto-reload threepenny-gui apps during development
When you are developing a threepenny app, you need to perform the
same dance after every change: recompile and restart the app, and
then manually reload a page in a browser. This becomes tiresome
very quickly. But with the help of ghcid
and with small changes
to your Haskell code you can completely automate this process.
All commands assume the simplest project layout with all the code
in Main.hs
. It’s a direct copy from threepenny-gui documentation,
and you can clone my example repository which also adds all the
necessary boilerplate.
Using ghcid
for reloading
Almost all of the problems can be solved by using ghcid
, which
will give us a freshly recompiled and started application every
time we’ll change the source code. Here is an example of a command
that is suitable for my example:
|
|
cabal new-repl
can be replaced with whatever you use to get a ghci REPL (stack ghci
,cabal repl
, etc.)--reload
switches (which can be specified multiple times) tell what files or directories should be watched for changes. You may want to monitor not only Haskell sources, but also some web assets like CSS and JavaScript files - so that reload will happen on all relevant changes- This is a command to start an app that is being sent to
ghci
after every successful (re)compilation - And we want to restart the whole
ghci
process when there are some changes to a cabal file, like new dependencies added or flags changed
Reloading browser page
That’s better, yet we still need to refresh our browser
manually. With small changes to Haskell code we can automate this
part also. Our main
usually looks like this:
main = do
appInit -- can take indeterminate amount of time
startGUI defaultConfig setup
To achieve automatic reloading we can introduce alternative main
which will be only used by ghcid
(with the -T
switch):
mainDevel = do
appInit -- can take indeterminate amount of time
forkIO $ do
threadDelay 500000 -- small delay so startGUI can start listening
refreshBrowserPage
startGUI defaultConfig setup
Now you need to come up with refreshBrowserPage
for the system
where you do development. Below you can see the dumbest
implementation which will work only for Firefox on Linux, and only
when xdotool
is installed. But it’s OK, as this is
development-only code that doesn’t even need to be robust or
universal.
My Linux/Firefox implementation uses the fact that threepenny
browser-side code tries to reload a page as soon as it looses
connection to a server, which usually results in Problem loading page
error (because recompiling and restarting application is not
as fast as page reload). The code just searches for all windows
which has Problem loading page
in their title and sends reload
hotkey (Ctrl-R
) to each of them.
import Control.Monad (forM_)
import Control.Monad.Catch
import System.Process (readProcess, callProcess)
import Text.Read (readMaybe)
refreshBrowserPage :: IO ()
refreshBrowserPage = do
maybeWindows :: Either SomeException String <- try
(readProcess "xdotool"
["search", "--all", "--name", "Problem loading"] "")
case maybeWindows of
Left _ -> return ()
Right idStr -> forM_ (lines idStr) $ \windowId ->
case readMaybe windowId of
Nothing -> return ()
Just (n :: Integer) -> do
putStrLn $ show n
callProcess "xdotool" ["key", "--window", show n, "CTRL+R"]
return ()
And this is all that’s needed to see your changes in browser almost immediately after you save an edited file.