diff --git a/Base.hs b/Base.hs
--- a/Base.hs
+++ b/Base.hs
@@ -35,7 +35,7 @@
 import Data.Foldable 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.List as X
 import Data.Maybe as X
 import Data.String as X
 import Data.Traversable as X
diff --git a/Core.hs b/Core.hs
--- a/Core.hs
+++ b/Core.hs
@@ -66,7 +66,6 @@
 import System.Clock             (TimeSpec(..), diffTimeSpec)
 import System.Random            (randomIO)
 import System.FilePath          ((</>))
-import Data.List                (isInfixOf, tails)
 
 import System.Posix.Process     (exitImmediately)
 
@@ -115,7 +114,7 @@
     loadConfig
 
     when (0 <= (snd . bounds $ fs)) do
-        if mode == Random then playRandom else playCur
+        if mode == Random then modifySTM jumpToRandom else playCur
     when (not playNow) pause
 
     run         -- won't restart if this fails!
@@ -405,11 +404,6 @@
     let fe = music st ! cursor st
     in P.intercalate (P.singleton '/') [dname $ folders st ! fdir fe, fbase fe]
 
-
--- | Play a random song
-playRandom :: IO ()
-playRandom = modifySTM jumpToRandom
-
 -- | Jump to a random song
 jumpToRandom :: HState -> IO HState
 jumpToRandom st = do
@@ -421,29 +415,25 @@
 -- If we're at the beginning, and loop mode is on, then loop to the end
 -- If we're in random mode, play the next random track
 playPrev :: IO ()
-playPrev = do
-    md <- getsST mode
-    if md == Random then playRandom else
-      modifySTM $ \st -> do
-      let i   = current st
-      if
-        | i > 0             -> playAtN st (subtract 1)      -- just the prev track
-        | mode st == Loop   -> playAtN st (const (size st - 1))  -- maybe loop
-        | otherwise         -> pure    st            -- else stop at end
+playPrev = modifySTM \st -> case mode st of
+    Random -> jumpToRandom st
+    Single -> pure st
+    _ | current st > 0
+           -> playAtN st (subtract 1)
+    Loop   -> playAtN st (const (size st - 1))
+    Once   -> pure st
 
 -- | Play the song following the current song, if we're not at the end
 -- If we're at the end, and loop mode is on, then loop to the start
 -- If we're in random mode, play the next random track
 playNext :: IO ()
-playNext = do
-    md <- getsST mode
-    if md == Random then playRandom else
-      modifySTM $ \st -> do
-      let i   = current st
-      if
-        | i < size st - 1   -> playAtN st (+ 1)      -- just the next track
-        | mode st == Loop   -> playAtN st (const 0)  -- maybe loop
-        | otherwise         -> pure    st            -- else stop at end
+playNext = modifySTM \st -> case mode st of
+    Random -> jumpToRandom st
+    Single -> pure st
+    _ | current st < size st - 1
+           -> playAtN st (+ 1)
+    Loop   -> playAtN st (const 0)
+    Once   -> pure st
 
 -- | Generic next song selection
 -- If the cursor and current are currently the same, continue that.
@@ -647,12 +637,11 @@
     if b then do
         str' <- readFile f
         str <- let (old, new) = ("hmp3_helpscreen", "hmp3_modals") in
-            if old `isInfixOf` str'
-            then do
-                warnA $ old ++ " is now " ++ new ++ " in style.conf"
-                let (ix, rest) = head $ filter (\ (_, s) -> old `isPrefixOf` s) $ zip [0..] $ tails str'
-                pure $ take ix str' ++ new ++ drop (length old) rest
-            else pure str'
+            case findIndex (old `isPrefixOf`) $ tails str' of
+                Just ix -> do
+                    warnA $ old ++ " is now " ++ new ++ " in style.conf"
+                    pure $ take ix str' ++ new ++ drop (ix + length old) str'
+                _ -> pure str'
         case readMaybe str of
             Nothing  -> do
                 warnA "Parse error in style.conf"
diff --git a/FastIO.hs b/FastIO.hs
--- a/FastIO.hs
+++ b/FastIO.hs
@@ -26,9 +26,7 @@
 
 import Syntax                   (Pretty(ppr))
 
-import qualified Data.ByteString.Char8 as P
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Internal as B
+import qualified Data.ByteString.Char8 as B
 
 import System.Posix.Files.ByteString
 import System.Posix.Directory.ByteString
@@ -43,22 +41,22 @@
 
 -- | Packed string version of basename
 basenameP :: ByteString -> ByteString
-basenameP fps = case P.elemIndexEnd '/' fps of
+basenameP fps = case B.elemIndexEnd '/' fps of
     Nothing -> fps
-    Just i  -> P.drop (i+1) fps
+    Just i  -> B.drop (i+1) fps
 {-# INLINE basenameP #-}
 
 dirnameP :: ByteString -> ByteString
-dirnameP fps = case P.elemIndexEnd '/' fps of
+dirnameP fps = case B.elemIndexEnd '/' fps of
     Nothing -> "."
-    Just i  -> P.take i fps
+    Just i  -> B.take i fps
 {-# INLINE dirnameP #-}
 
 -- | Packed version of listDirectory
 packedGetDirectoryContents :: ByteString -> IO [ByteString]
 packedGetDirectoryContents fp = bracket (openDirStream fp) closeDirStream
     $ \ds -> fmap (filter (\p -> p/="." && p/=".."))
-        $ sequenceWhile (not . P.null) $ repeat $ readDirStream ds
+        $ sequenceWhile (not . B.null) $ repeat $ readDirStream ds
 
 doesFileExist :: ByteString -> IO Bool
 doesFileExist fp = catch @SomeException
@@ -72,7 +70,7 @@
 
 packedFileNameEndClean :: ByteString -> ByteString
 packedFileNameEndClean name =
-  case P.unsnoc name of
+  case B.unsnoc name of
     Just (name', ec) | ec == '\\' || ec == '/'
          -> packedFileNameEndClean name'
     _    -> name
@@ -105,17 +103,12 @@
 -- ---------------------------------------------------------------------
 -- | Send a msg over the channel to the decoder
 send :: Pretty a => Handle -> a -> IO ()
-send h m = P.hPut h (ppr m) >> P.hPut h "\n" >> hFlush h
+send h m = B.hPut h (ppr m) >> B.hPut h "\n" >> hFlush h
 
 ------------------------------------------------------------------------ 
 
--- | 'dropSpaceEnd' efficiently returns the 'ByteString' argument with
--- white space removed from the end. I.e.,
--- 
--- > reverse . (dropWhile isSpace) . reverse == dropSpaceEnd
-dropSpaceEnd :: ByteString -> ByteString
-{-# INLINE dropSpaceEnd #-}
-dropSpaceEnd bs = P.take (P.length bs - count) bs where
-    count = B.foldl' go 0 bs
-    go n c = if B.isSpaceWord8 c then n+1 else 0
+-- ---------------------------------------------------------------------
+trim :: ByteString -> ByteString
+{-# INLINE trim #-}
+trim = B.dropWhileEnd isSpace . B.dropSpace
 
diff --git a/Keymap.hs b/Keymap.hs
--- a/Keymap.hs
+++ b/Keymap.hs
@@ -33,7 +33,7 @@
 module Keymap where
 
 import Prelude ()
-import Base hiding (all)
+import Base hiding (all, delete)
 
 import Core
 import Config       (package)
diff --git a/Lexer.hs b/Lexer.hs
--- a/Lexer.hs
+++ b/Lexer.hs
@@ -25,7 +25,7 @@
 import Base
 
 import Syntax   (Msg(..),Status(..),Frame(..),Info(..),Id3(..),File(..),Tag(..))
-import FastIO   (FiltHandle(..), checkF, getPacket, dropSpaceEnd)
+import FastIO   (FiltHandle(..), checkF, getPacket, trim)
 
 import qualified Data.ByteString.Char8 as P
 import qualified Data.ByteString.UTF8 as UTF8
@@ -93,7 +93,7 @@
 -- Track info if ID fields are in the file, otherwise file name.
 -- 30 chars per field?
 doI :: ByteString -> Msg
-doI s = let f = dropSpaceEnd . P.dropWhile isSpace $ s
+doI s = let f = trim s
         in case P.take 4 f of
             cs | cs == "ID3:" -> F . File $
                     let ttl = toId id3 . splitUp . P.drop 4 $ f
@@ -137,7 +137,7 @@
         -- strip spaces, and decide if UTF-8 or ISO-8859-1
         normalise :: ByteString -> ByteString
         normalise raw =
-            let bs = P.dropWhile isSpace . dropSpaceEnd $ raw
+            let bs = trim raw
             in if UTF8.replacement_char `elem` UTF8.toString bs
                 then UTF8.fromString $ P.unpack bs
                 else bs
diff --git a/State.hs b/State.hs
--- a/State.hs
+++ b/State.hs
@@ -119,7 +119,7 @@
        ,config       = Config.defaultStyle
        ,boottime     = 0
        ,status       = Stopped
-       ,mode         = Normal
+       ,mode         = Once
        ,minibuffer   = Fast mempty defaultSty
        ,uptime       = mempty
        ,drawLock     = unsafePerformIO (newMVar ())
diff --git a/Syntax.hs b/Syntax.hs
--- a/Syntax.hs
+++ b/Syntax.hs
@@ -139,7 +139,7 @@
             | Playing
     deriving stock (Eq, Show)
 
-data Mode = Normal | Loop | Random
+data Mode = Once | Loop | Random | Single
     deriving stock (Eq, Bounded, Enum, Show, Read)
 
 ------------------------------------------------------------------------
diff --git a/Tree.hs b/Tree.hs
--- a/Tree.hs
+++ b/Tree.hs
@@ -25,7 +25,7 @@
 
 module Tree where
 
-import Base
+import Base hiding (partition)
 
 import FastIO
 import qualified Data.ByteString.Char8 as P
diff --git a/UI.hs b/UI.hs
--- a/UI.hs
+++ b/UI.hs
@@ -414,7 +414,8 @@
     draw dd = PMode2 case mode $ drawState dd of
                         Random  -> "rand"
                         Loop    -> "loop"
-                        Normal  -> "once"
+                        Once    -> "once"
+                        Single  -> "sing"
 
 ------------------------------------------------------------------------
 
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.16.2
+version:        2.17.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
