Add an optional verbose flag to the Dhall configuration.

It is OR'd with the --verbose CLI flag. Defaults to False
when omitted, via record merge with a defaults record in
readConfig.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Change-Id: Ic3d3ef408e97a2b083c910eee685e57043ee6f7e
diff --git a/config.example.dhall b/config.example.dhall
index 42451ac..c3b0476 100644
--- a/config.example.dhall
+++ b/config.example.dhall
@@ -1,6 +1,7 @@
 let home = "/Users/mulk" in
 
 { host = "mulkinator"
+, verbose = False
 , stashes =
     [ { name = "mulk.tar"
       , baseDir = home
diff --git a/src/Mulkup/Config.hs b/src/Mulkup/Config.hs
index 8356824..b1db1a9 100644
--- a/src/Mulkup/Config.hs
+++ b/src/Mulkup/Config.hs
@@ -33,7 +33,7 @@
 
 --- MulkupConfig ---
 
-data MulkupConfig = MulkupConfig {host :: Text, stashes :: [StashConfig]}
+data MulkupConfig = MulkupConfig {host :: Text, stashes :: [StashConfig], verbose :: Bool}
   deriving stock (Generic, Show)
   deriving anyclass (FromDhall)
 
@@ -41,5 +41,7 @@
 
 --- readConfig ---
 
+-- | Reads a Dhall config, merging the user's record over a defaults
+-- record so optional fields can be omitted entirely.
 readConfig :: Text -> IO MulkupConfig
-readConfig = Dhall.input auto
+readConfig text = Dhall.input auto ("{ verbose = False } // (" <> text <> ")")
diff --git a/src/Mulkup/Main.hs b/src/Mulkup/Main.hs
index 32c63ea..84aff6a 100644
--- a/src/Mulkup/Main.hs
+++ b/src/Mulkup/Main.hs
@@ -27,19 +27,20 @@
 main :: IO ()
 main = do
   flags <- execParser $ info (flagParser <**> helper) fullDesc
+  config <- readConfig "./config.dhall"
+
+  let effectiveFlags = flags & #verbose %~ (|| config ^. #verbose)
   let messageAction =
-        if verbose flags
+        if effectiveFlags ^. #verbose
           then richMessageAction
           else simpleMessageAction
 
-  config <- readConfig "./config.dhall"
-
   result <-
     main'
       & runBupstash
       & runLogAction @IO messageAction
       & runReader (config :: MulkupConfig)
-      & runReader flags
+      & runReader effectiveFlags
       & errorToIOFinal @Text
       & embedToFinal @IO
       & runFinal @IO
diff --git a/test/Mulkup/ConfigSpec.hs b/test/Mulkup/ConfigSpec.hs
index 142d130..42d9d5f 100644
--- a/test/Mulkup/ConfigSpec.hs
+++ b/test/Mulkup/ConfigSpec.hs
@@ -11,7 +11,7 @@
 cases =
   testGroup
     "ConfigSpec"
-    [unit_simpleConfig]
+    [unit_simpleConfig, unit_verboseOmitted]
 
 unit_simpleConfig :: TestTree
 unit_simpleConfig = testCase "unit_simpleConfig" $ do
@@ -21,6 +21,8 @@
       "\
       \{ host = \"atmon\" \
       \ \
+      \, verbose = False \
+      \ \
       \, stashes = \
       \    [ { name = \"mulk\" \
       \       \
@@ -46,3 +48,26 @@
       \      } \
       \    ] \
       \}"
+
+unit_verboseOmitted :: TestTree
+unit_verboseOmitted = testCase "unit_verboseOmitted" $ do
+  config <- readConfig exampleConfigText
+  verbose config @?= False
+  where
+    exampleConfigText =
+      "\
+      \{ host = \"atmon\" \
+      \ \
+      \, stashes = \
+      \    [ { name = \"mulk\" \
+      \      , baseDir = \"/Users/mulk\" \
+      \      , tiers = \
+      \          { hourly  = { keep = 1 } \
+      \          , daily   = { keep = 1 } \
+      \          , weekly  = { keep = 1 } \
+      \          , monthly = { keep = 1 } \
+      \          } \
+      \      , exclusions = [] : List Text \
+      \      } \
+      \    ] \
+      \}"