diff --git a/Base.hs b/Base.hs
--- a/Base.hs
+++ b/Base.hs
@@ -1,7 +1,7 @@
 {-# LANGUAGE CPP #-}
 
 --
--- Copyright (c) 2020, 2021 Galen Huntington
+-- Copyright (c) 2020-2024 Galen Huntington
 --
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -33,7 +33,7 @@
 import Data.Char as X
 import Data.Fixed as X
 import Data.Foldable as X
-import Data.Functor as X
+import Data.Functor as X hiding (unzip)
 import Data.IORef as X
 import Data.List as X ((\\), group, groupBy, isPrefixOf, sort, sortBy, intersperse)
 import Data.Maybe as X
@@ -53,9 +53,6 @@
 
 discardErrors :: IO () -> IO ()
 discardErrors = X.handle @SomeException (\_ -> pure ())
-
-whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()
-whenJust mval f = case mval of Nothing -> pure (); Just v -> f v
 
 getMonoTime :: IO TimeSpec
 getMonoTime = getTime
diff --git a/Core.hs b/Core.hs
--- a/Core.hs
+++ b/Core.hs
@@ -2,7 +2,7 @@
 
 -- 
 -- Copyright (c) 2005-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
--- Copyright (c) 2008, 2019-2022 Galen Huntington
+-- Copyright (c) 2008, 2019-2024 Galen Huntington
 -- 
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -49,7 +49,7 @@
 import State
 import Style
 import FastIO               (send, FiltHandle(..), newFiltHandle)
-import Tree hiding (File,Dir)
+import Tree hiding (File, Dir)
 import qualified Tree (File,Dir)
 import qualified UI
 
@@ -77,10 +77,10 @@
 
 mp3Tool :: String
 mp3Tool =
-#ifdef MPG123
-    "mpg123"
-#else
+#ifdef MPG321
     "mpg321"
+#else
+    "mpg123"
 #endif
 
 ------------------------------------------------------------------------
@@ -88,7 +88,7 @@
 start :: Bool -> FileListSource -> IO ()
 start playNow ms = handle @SomeException (shutdown . Just . show) do
 
-    t0 <- forkIO mpgLoop    -- start this off early, to give mpg321 time to settle
+    t0 <- forkIO mpgLoop    -- start this off early, to give mpg123 time to settle
 
     c <- UI.start -- initialise curses
 
@@ -156,7 +156,7 @@
 
 ------------------------------------------------------------------------
 
--- | Process loop, launch mpg321, set the handles in the state
+-- | Process loop, launch mpg123, set the handles in the state
 -- and then wait for the process to die. If it does, restart it.
 --
 -- If we're unable to start at all, we should say something sensible
@@ -167,18 +167,17 @@
     mmpg <- findExecutable mp3Tool
     case mmpg of
       Nothing     -> quit (Just $ "Cannot find " ++ mp3Tool ++ " in path")
-      Just mpg321 -> do
+      Just mppath -> do
 
-        -- if we're never able to start mpg321, do something sensible
+        -- if we're never able to start mpg123, do something sensible
         --   TODO no need for this Maybe unpacking, just catch and rerun loop
-        mv <- catch (pure <$> runInteractiveProcess mpg321 ["-R","-"] Nothing Nothing)
+        mv <- catch (pure <$> runInteractiveProcess mppath ["-R", "-"] Nothing Nothing)
                     (\ (e :: SomeException) ->
                            do warnA ("Unable to start " ++ mp3Tool ++ ": " ++ show e)
                               pure Nothing)
-        case mv of
-            Nothing -> threadDelay (1000 * 500) >> mpgLoop
-            Just (hw, r, e, pid) -> do
 
+        flip (maybe (threadDelay 1_000_000)) mv \ (hw, r, e, pid) -> do
+
             mhw         <- newMVar hw
             mew         <- newMVar =<< newFiltHandle e
             mfilep      <- newMVar =<< newFiltHandle r
@@ -195,7 +194,7 @@
             catch @SomeException (void $ waitForProcess pid) (\_ -> pure ())
             stop <- getsST doNotResuscitate
             when stop exitSuccess
-            warnA $ "Restarting " ++ mpg321 ++ " ..."
+            warnA $ "Restarting " ++ mppath ++ " ..."
 
 ------------------------------------------------------------------------
 
@@ -247,7 +246,7 @@
 
 ------------------------------------------------------------------------
 
--- | Handle, and display errors produced by mpg321
+-- | Handle, and display errors produced by mpg123
 errorLoop :: IO ()
 errorLoop = runForever $ do
     s <- getsST errh >>= readMVar >>= hGetLine . filtHandle
@@ -519,14 +518,14 @@
 
 jumpToMatchFile :: Maybe String -> Bool -> IO ()
 jumpToMatchFile re sw = genericJumpToMatch re sw k sel
-    where k = \st -> (music st, if size st == 0 then -1 else cursor st, size st)
+    where k st = (music st, if size st == 0 then -1 else cursor st, size st)
           sel i _ = i
 
 jumpToMatch  :: Maybe String -> Bool -> IO ()
 jumpToMatch     re sw = genericJumpToMatch re sw k sel
-    where k = \st -> (folders st
-                     ,if size st == 0 then -1 else fdir (music st ! cursor st)
-                     ,1 + (snd . bounds $ folders st))
+    where k st = (folders st
+                     , if size st == 0 then -1 else fdir (music st ! cursor st)
+                     , 1 + (snd . bounds $ folders st))
           sel i st = dlo (folders st ! i)
 
 genericJumpToMatch :: Lookup a
diff --git a/Keymap.hs b/Keymap.hs
--- a/Keymap.hs
+++ b/Keymap.hs
@@ -1,6 +1,6 @@
 -- 
 -- Copyright (c) 2004-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
--- Copyright (c) 2008, 2019-2022 Galen Huntington
+-- Copyright (c) 2008, 2019-2024 Galen Huntington
 -- 
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -169,7 +169,7 @@
     handleKey base off cs st =
         (with do
             ph <- getsST playHist
-            whenJust
+            for_
                 do ph Seq.!? (fromEnum (head cs) - (fromEnum base - off))
                 do jump . snd
             hideHist
@@ -191,7 +191,7 @@
 -- "Key"s seem to be inscrutable and incomparable.
 -- So, add an orphan instance to help translate to chars.
 
-deriving instance Ord Key
+deriving stock instance Ord Key
 
 charToKey :: Char -> Key
 charToKey = decodeKey . toEnum . fromEnum
diff --git a/Lexer.hs b/Lexer.hs
--- a/Lexer.hs
+++ b/Lexer.hs
@@ -1,6 +1,6 @@
 -- 
 -- Copyright (c) 2005-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
--- Copyright (c) 2008, 2019, 2020 Galen Huntington
+-- Copyright (c) 2008, 2019-2024 Galen Huntington
 -- 
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -18,7 +18,7 @@
 -- 02111-1307, USA.
 -- 
 
--- Lexer for mpg321 messages
+-- Lexer for mpg123 messages
 
 module Lexer ( parser ) where
 
@@ -45,7 +45,7 @@
                 '0' -> Stopped
                 '1' -> Paused
                 '2' -> Playing
-                '3' -> Stopped  -- used by mpg321
+                '3' -> Stopped  -- used by mpg123
                 _ -> Playing
                 -- _ -> error "Invalid Status"
 
@@ -95,7 +95,7 @@
         in case P.take 4 f of
             cs | cs == "ID3:" -> F . File $
                     let ttl = toId id3 . splitUp . P.drop 4 $ f
-                    -- mpg321 sometimes returns null titles
+                    -- mpg123 sometimes returns null titles
                     in if P.null (id3title ttl) then Left f else Right ttl
                | otherwise    -> F . File . Left $ f
     where
@@ -162,6 +162,6 @@
             b <- lift $ checkF h
             if b then pure $ doF m else skip
         'P' -> pure $ doP m
-        'E' -> throwError $ Just $ "mpg321 error: " ++ P.unpack m
+        'E' -> throwError $ Just $ "mpg123 error: " ++ P.unpack m
         _   -> skip
 
diff --git a/Main.hs b/Main.hs
--- a/Main.hs
+++ b/Main.hs
@@ -1,7 +1,7 @@
 -- 
 -- Copyright (c) Don Stewart 2004-2008.
 -- Copyright (c) Tuomo Valkonen 2004.
--- Copyright (c) 2019-2021 Galen Huntington
+-- Copyright (c) 2019-2024 Galen Huntington
 --
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -101,7 +101,7 @@
 --
 main :: IO ()
 main = do
-    (playNow, files) <- doArgs =<< map fromString <$> getArgs
+    (playNow, files) <- doArgs . map fromString =<< getArgs
     initSignals
     start playNow files -- never returns
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -20,11 +20,12 @@
 versions, which were only minor changes, mostly the automated
 regeneration of a `configure` file (now gone).
 
-*  The code has been updated to compile under recent GHC (currently
-8.6, 8.8, 8.10, 9.0, and 9.2) and libraries.  This required rewriting
-or entirely replacing large sections, mainly low-level optimizations.
+*  The code has been updated to compile under recent GHC (tested
+through 9.10) and libraries.  This required rewriting or entirely
+replacing large sections, mainly low-level optimizations.
 
-*  I added support for building with Stack.
+*  I added support for building with Stack.  It can also be installed
+from Nix.
 
 *  There is a public GitHub issue tracker, and a GitHub action to
 continuously test builds.
@@ -55,15 +56,15 @@
 ##  Installation
 
 Either `cabal install` or `stack install` will build a binary.
-You will need to have `mpg321` installed, which is free software
-and widely available in package managers.  Alternatively, `mpg123`
-can be used by compiling with the `-DMPG123` option, but, while your
-mileage may vary, in my experience it doesn’t work as well.
+You will need to have `mpg123` installed, which is free software and
+widely available in package managers.  Alternatively, `mpg321` can
+be used by compiling with the `-DMPG321` option.  In my experience,
+the latter worked better, but it has not been updated since 2012 and
+is no longer available on many systems.
 
 The build depends on the package `hscurses`, which in turn requires
-curses dev files.  In Ubuntu/Debian, for example, these can be
-obtained by installing `libncurses5-dev`.  You probably also need
-`libncursesw5-dev`.
+curses dev files.  In Ubuntu/Debian, for example, these can be obtained
+by installing `libncurses-dev`.
 
 
 ##  Use
@@ -83,9 +84,9 @@
 message with command line options.
 
 A color scheme can be specified by writing out a `Config { .. }`
-value in `~/.config/hmp3/style.conf` or the equivalent in your
-XDG config directory.  See `Style.hs` for the definition.  The `l`
-command hot-reloads this configuration.
+value in `~/.config/hmp3/style.conf` (or wherever your XDG config is).
+See `Style.hs` for the definition.  The `l` command hot-reloads this
+configuration.
 
 
 ##  Original authorship list
diff --git a/State.hs b/State.hs
--- a/State.hs
+++ b/State.hs
@@ -1,6 +1,6 @@
 -- 
 -- Copyright (c) 2004-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
--- Copyright (c) 2019, 2020 Galen Huntington
+-- Copyright (c) 2019-2024 Galen Huntington
 -- 
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -72,7 +72,7 @@
        ,boottime        :: !TimeSpec
        ,regex           :: !(Maybe (Regex,Bool)) -- most recent search pattern and direction
        ,xterm           :: !Bool
-       ,doNotResuscitate :: !Bool                -- should we just let mpg321 die?
+       ,doNotResuscitate :: !Bool                -- should we just let mpg123 die?
        ,playHist        :: Seq (TimeSpec, Int)  -- limited history of songs played
        ,config          :: !UIStyle             -- config values
 
@@ -113,7 +113,7 @@
        ,exitVisible      = False
        ,miniFocused      = False
        ,xterm            = False
-       ,doNotResuscitate = False    -- mpg321 should be restarted
+       ,doNotResuscitate = False    -- mpg123 should be restarted
 
        ,playHist     = mempty
        ,config       = Config.defaultStyle
diff --git a/Syntax.hs b/Syntax.hs
--- a/Syntax.hs
+++ b/Syntax.hs
@@ -1,6 +1,6 @@
 -- 
 -- Copyright (c) 2005-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
--- Copyright (c) 2019, 2020 Galen Huntington
+-- Copyright (c) 2019-2024 Galen Huntington
 -- 
 -- This program is free software; you can redistribute it and/or
 -- modify it under the terms of the GNU General Public License as
@@ -22,8 +22,6 @@
 -- abstract syntax for mpg123/321 'remote control' commands, so we get
 -- type safe messaging, and parsing of results
 --
--- See 'README.remote' distributed with mpg321.
---
 
 module Syntax where
 
@@ -56,7 +54,7 @@
 instance Pretty Pause where
     ppr Pause = "PAUSE"
 
--- Quits mpg321.
+-- Quits mpg123.
 data Quit = Quit
 
 instance Pretty Quit where
@@ -142,7 +140,7 @@
     deriving stock (Eq, Show)
 
 data Mode = Normal | Loop | Random
-    deriving stock (Eq,Bounded,Enum)
+    deriving stock (Eq, Bounded, Enum)
 
 ------------------------------------------------------------------------
 
diff --git a/UI.hs b/UI.hs
--- a/UI.hs
+++ b/UI.hs
@@ -54,7 +54,6 @@
 
 import Data.Array               ((!), bounds, Array, listArray)
 import Data.Array.Base          (unsafeAt)
-import Data.List                (intercalate)
 import System.IO                (stderr, hFlush)
 import System.Posix.Signals     (raiseSignal, sigTSTP, installHandler, Handler(..))
 
@@ -262,7 +261,7 @@
         PId3 a  = draw dd
         PInfo b = draw dd
         s       = UTF8.toString a
-        line | gap >= 0 = U s : (B $ spaces gap) : right
+        line | gap >= 0 = U s : B (spaces gap) : right
              | True     = U (ellipsize lim s) : right
             where lim = x - 5 - (if showId3 then P.length b else -1)
                   gap = lim - displayWidth s
@@ -308,7 +307,7 @@
             f cs ps = UTF8.fromString $ forceWidth wd
                         $ forceWidth clen cmds <> ps where
                 clen = max 4 $ round $ fromIntegral wd * (0.2::Float)
-                cmds = intercalate " " $ "" : map pprIt cs
+                cmds = unwords $ "" : map pprIt cs
                 pprIt c = case c of
                       '\n'            -> "Enter"
                       '\f'            -> "^L"
@@ -346,7 +345,7 @@
             blank = Fast (UTF8.fromString $ forceWidth wd "") sty
             padl = replicate ((wd - 9) `div` 2) ' '
             msg = forceWidth wd $ padl ++ "Exit (y)?"
-        pure $ (wd, [blank, Fast (UTF8.fromString msg) sty, blank])
+        pure (wd, [blank, Fast (UTF8.fromString msg) sty, blank])
 
 ------------------------------------------------------------------------
 
@@ -600,7 +599,7 @@
 --
 renderModal :: forall me. ModalElement me => HState -> Size -> IO ()
 renderModal st (Size h w) = do
-   whenJust (drawModal @me (modals $ config st) w st) \(mw, modal') -> do
+   for_ (drawModal @me (modals $ config st) w st) \(mw, modal') -> do
        let hoffset = max 0 $ (w - mw) `div` 2
            mlines  = min h $ length modal'
            voffset = (h - mlines) `div` 2
diff --git a/hmp3-ng.cabal b/hmp3-ng.cabal
--- a/hmp3-ng.cabal
+++ b/hmp3-ng.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.2
 
 name:           hmp3-ng
-version:        2.14.3
+version:        2.15.0
 synopsis:       A 2019 fork of an ncurses mp3 player written in Haskell
 description:    An mp3 player with a curses frontend.  Playlists are populated by
                 passing file and directory names on the command line.  'h' displays
@@ -54,6 +54,7 @@
       LambdaCase
       MultiWayIf
       StandaloneDeriving
+      NumericUnderscores
   ghc-options: -Wall -funbox-strict-fields -threaded -Wno-unused-do-bind
   extra-libraries:
       ncursesw
