packages feed

photoname 5.4 → 5.5

raw patch · 16 files changed

+282/−263 lines, 16 filesdep +ansi-terminaldep +co-log-simpledep +prettyprinterdep −ansi-wl-pprintdep −hsloggerdep ~mtldep ~optparse-applicativedep ~regex-posixPVP ok

version bump matches the API change (PVP)

Dependencies added: ansi-terminal, co-log-simple, prettyprinter, text

Dependencies removed: ansi-wl-pprint, hslogger

Dependency ranges changed: mtl, optparse-applicative, regex-posix, tasty, tasty-hunit, tasty-quickcheck, unix

API changes (from Hackage documentation)

- Photoname.Common: [$sel:artist:Options] :: Options -> Maybe Artist
- Photoname.Common: [$sel:config:Options] :: Options -> Maybe ConfigPath
- Photoname.Common: [$sel:copy:Options] :: Options -> CopySwitch
- Photoname.Common: [$sel:extension:Options] :: Options -> Extension
- Photoname.Common: [$sel:formatter:Options] :: Options -> DateFormatter
- Photoname.Common: [$sel:links:Options] :: Options -> Links
- Photoname.Common: [$sel:move:Options] :: Options -> MoveSwitch
- Photoname.Common: [$sel:noAction:Options] :: Options -> NoActionSwitch
- Photoname.Common: [$sel:noDirs:Options] :: Options -> NoDirsSwitch
- Photoname.Common: [$sel:parentDir:Options] :: Options -> ParentDir
- Photoname.Common: [$sel:paths:Options] :: Options -> [FilePath]
- Photoname.Common: [$sel:prefix:Options] :: Options -> Prefix
- Photoname.Common: [$sel:suffix:Options] :: Options -> Suffix
- Photoname.Common: [$sel:v:CopySwitch] :: CopySwitch -> Bool
- Photoname.Common: [$sel:v:MoveSwitch] :: MoveSwitch -> Bool
- Photoname.Common: [$sel:v:NoActionSwitch] :: NoActionSwitch -> Bool
- Photoname.Common: [$sel:v:NoDirsSwitch] :: NoDirsSwitch -> Bool
- Photoname.Common: [$sel:v:ParentDir] :: ParentDir -> FilePath
- Photoname.Common: [$sel:v:Prefix] :: Prefix -> String
- Photoname.Common: [$sel:v:SrcPath] :: SrcPath -> FilePath
- Photoname.Common: [$sel:v:Suffix] :: Suffix -> String
- Photoname.Common: [$sel:verbosity:Options] :: Options -> Verbosity
- Photoname.Common: ask :: MonadReader r m => m r
- Photoname.Common: asks :: MonadReader r m => (r -> a) -> m a
- Photoname.Common: class Monad m => MonadError e (m :: Type -> Type) | m -> e
- Photoname.Common: liftIO :: MonadIO m => IO a -> m a
- Photoname.Common: runRename :: Options -> Ph a -> IO (Either String a)
- Photoname.Common: throwError :: MonadError e m => e -> m a
- Photoname.Common: type Ph a = ReaderT Options (ExceptT String IO) a
- Photoname.Log: alertM :: String -> String -> IO ()
- Photoname.Log: criticalM :: String -> String -> IO ()
- Photoname.Log: debugM :: String -> String -> IO ()
- Photoname.Log: emergencyM :: String -> String -> IO ()
- Photoname.Log: errorM :: String -> String -> IO ()
- Photoname.Log: infoM :: String -> String -> IO ()
- Photoname.Log: initLogging :: Verbosity -> IO ()
- Photoname.Log: lname :: String
- Photoname.Log: logTest :: IO ()
- Photoname.Log: noticeM :: String -> String -> IO ()
- Photoname.Log: type LogFunction = String -> String -> IO ()
- Photoname.Log: warningM :: String -> String -> IO ()
+ Photoname.Common: Debug :: Severity
+ Photoname.Common: Info :: Severity
+ Photoname.Common: Notice :: Severity
+ Photoname.Common: [artist] :: Options -> Maybe Artist
+ Photoname.Common: [config] :: Options -> Maybe ConfigPath
+ Photoname.Common: [copy] :: Options -> CopySwitch
+ Photoname.Common: [extension] :: Options -> Extension
+ Photoname.Common: [formatter] :: Options -> DateFormatter
+ Photoname.Common: [links] :: Options -> Links
+ Photoname.Common: [move] :: Options -> MoveSwitch
+ Photoname.Common: [noAction] :: Options -> NoActionSwitch
+ Photoname.Common: [noDirs] :: Options -> NoDirsSwitch
+ Photoname.Common: [parentDir] :: Options -> ParentDir
+ Photoname.Common: [paths] :: Options -> [FilePath]
+ Photoname.Common: [prefix] :: Options -> Prefix
+ Photoname.Common: [suffix] :: Options -> Suffix
+ Photoname.Common: [v] :: Suffix -> String
+ Photoname.Common: [verbosity] :: Options -> Verbosity
+ Photoname.Common: data Severity
+ Photoname.Log: logger :: forall (m :: Type -> Type). MonadIO m => Verbosity -> LogAction m Message
+ Photoname.Monad: Env :: !Options -> !LogAction m Message -> Env (m :: Type -> Type)
+ Photoname.Monad: [envLogAction] :: Env (m :: Type -> Type) -> !LogAction m Message
+ Photoname.Monad: [envOptions] :: Env (m :: Type -> Type) -> !Options
+ Photoname.Monad: ask :: MonadReader r m => m r
+ Photoname.Monad: asks :: MonadReader r m => (r -> a) -> m a
+ Photoname.Monad: data Env (m :: Type -> Type)
+ Photoname.Monad: data Ph a
+ Photoname.Monad: instance Colog.Core.Class.HasLog (Photoname.Monad.Env m) Colog.Simple.Message.Message m
+ Photoname.Monad: instance Control.Monad.IO.Class.MonadIO Photoname.Monad.Ph
+ Photoname.Monad: instance Control.Monad.Reader.Class.MonadReader (Photoname.Monad.Env Photoname.Monad.Ph) Photoname.Monad.Ph
+ Photoname.Monad: instance GHC.Base.Applicative Photoname.Monad.Ph
+ Photoname.Monad: instance GHC.Base.Functor Photoname.Monad.Ph
+ Photoname.Monad: instance GHC.Base.Monad Photoname.Monad.Ph
+ Photoname.Monad: liftIO :: MonadIO m => IO a -> m a
+ Photoname.Monad: runRename :: Env Ph -> Ph a -> IO a
- Photoname.Common: Verbose :: Priority -> Verbosity
+ Photoname.Common: Verbose :: Severity -> Verbosity
- Photoname.CopyLink: createNewLink :: PhDate -> SrcPath -> Ph DestPath
+ Photoname.CopyLink: createNewLink :: PhDate -> SrcPath -> Ph (Maybe DestPath)
- Photoname.Links: describeHardLinkPolicy :: Links -> IO ()
+ Photoname.Links: describeHardLinkPolicy :: Links -> Ph ()

Files

README.md view
@@ -15,12 +15,12 @@  ## Getting binaries -photoname is available for Linux in AppImage form [from github](https://github.com/dino-/photoname/releases)+photoname is available for Linux in AppImage form [from codeberg](https://codeberg.org/dinofp/photoname/releases)   ## Getting source -- Get the source with git: `$ git clone https://github.com/dino-/photoname.git`+- Get the source with git: `$ git clone https://codeberg.org/dinofp/photoname.git` - Download the cabalized source package [from Hackage](http://hackage.haskell.org/package/photoname)  And once you have it, building the usual way:@@ -29,13 +29,13 @@     $ stack test --ta all     $ stack run -If you're just looking, [browse the source](https://github.com/dino-/photoname)+If you're just looking, [browse the source](https://codeberg.org/dinofp/photoname)   ## Building for release  For Linux builds, I recommend using the excellent-[hsinstall](https://github.com/dino-/hsinstall) utility which will simplify+[hsinstall](https://codeberg.org/dinofp/hsinstall) utility which will simplify creation of an [AppImage](https://appimage.org/) executable.  
changelog.md view
@@ -1,3 +1,19 @@+5.5 (2025-08-11)++  * Changed logging from hslogger to co-log-simple+  * Extensive redesign work because of the logging changes+  * Removed unused and duplicate LANGUAGE directives+  * Replaced remaining ++ with <>+  * Implemented hlint suggestions+  * Fixed formatting issues in the cabal file+  * Moved Stackage snapshot up to lts-22.6+++5.4 (2024-11-21)++  * Reverted lookup order of EXIF date tags++ 5.3 (2024-11-18)    * Now looking for the 3 EXIF tags when using exiv2
photoname.cabal view
@@ -1,17 +1,16 @@ cabal-version: 2.2  name: photoname-version: 5.4+version: 5.5 synopsis: Rename photo image files based on EXIF shoot date description: Command-line utility for renaming/moving photo image files based   on EXIF tags.-category: Application, Console-stability: stable-homepage: https://github.com/dino-/photoname-bug-reports: https://github.com/dino-/photoname/issues author: Dino Morelli maintainer: Dino Morelli <dino@ui3.info> copyright: 2007 Dino Morelli+category: Application, Console+homepage: https://codeberg.org/dinofp/photoname+bug-reports: https://codeberg.org/dinofp/photoname/issues license: ISC license-file: LICENSE build-type: Simple@@ -37,7 +36,7 @@  source-repository head   type: git-  location: https://github.com/dino-/photoname+  location: https://codeberg.org/dinofp/photoname.git  common lang   default-language: Haskell2010@@ -49,6 +48,8 @@     DeriveLift     DeriveTraversable     EmptyCase+    FlexibleContexts+    FlexibleInstances     GeneralizedNewtypeDeriving     ImportQualifiedPost     InstanceSigs@@ -58,6 +59,7 @@     MultiWayIf     NamedFieldPuns     NumericUnderscores+    OverloadedStrings     ScopedTypeVariables     StandaloneDeriving     TupleSections@@ -71,9 +73,10 @@     -Wredundant-constraints   build-depends:       base >=4.5 && <5.0-    , directory >= 1.3.6 && < 1.4-    , filepath >= 1.4.2 && < 1.5-    , unix >= 2.7.2 && < 2.8+    , co-log-simple >=1.1.0 && <2+    , directory >=1.3.6 && <1.4+    , filepath >=1.4.2 && <1.5+    , unix >=2.7.2 && <3  library   import: lang@@ -85,6 +88,7 @@     Photoname.Exiv2     Photoname.Links     Photoname.Log+    Photoname.Monad   other-modules:     Paths_photoname   autogen-modules:@@ -92,14 +96,15 @@   hs-source-dirs:     src/lib   build-depends:-      containers >= 0.6.5 && < 0.7-    , formatting >= 7.2.0 && < 7.3-    , hsexif >= 0.6.1 && < 0.7-    , hslogger >= 1.3.1 && < 1.4-    , mtl >= 2.2.2 && < 2.3-    , parsec >= 3.1.15 && < 3.2-    , process >= 1.6.16 && < 1.7-    , time >= 1.11.1 && < 1.13+      ansi-terminal >=1.0.2 && <2+    , containers >=0.6.5 && <0.7+    , formatting >=7.2.0 && <7.3+    , hsexif >=0.6.1 && <0.7+    , mtl >=2.3.1 && <2.4+    , parsec >=3.1.15 && <3.2+    , process >=1.6.16 && <1.7+    , text >=2.0.2 && <2.2+    , time >=1.11.1 && <1.13  executable photoname   import: lang@@ -112,12 +117,11 @@   hs-source-dirs:       src/app   build-depends:-      ansi-wl-pprint >= 0.6.9 && < 0.7-    , formatting >= 7.2.0 && < 7.3-    , heredoc >= 0.2.0 && < 0.3-    , hslogger >= 1.3.1 && < 1.4-    , optparse-applicative >= 0.17.1 && < 0.18+      formatting >=7.2.0 && <7.3+    , heredoc >=0.2.0 && <0.3+    , optparse-applicative >=0.17.1 && <0.19     , photoname+    , prettyprinter >=1.7.1 && <2  test-suite photoname-tests   import: lang@@ -131,9 +135,9 @@     src/tests   build-depends:       photoname-    , process >= 1.6.16 && < 1.7-    , regex-posix-    , tasty-    , tasty-hunit-    , tasty-quickcheck-    , time >= 1.11.1 && < 1.13+    , process >=1.6.16 && <1.7+    , regex-posix >=0.96.0.1 && <1+    , tasty >=1.4.3 && <1.6+    , tasty-hunit >=0.10.1 && <0.11+    , tasty-quickcheck >=0.10.2 && <0.12+    , time >=1.11.1 && <1.13
src/app/Photoname/Opts.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}+{-# LANGUAGE QuasiQuotes #-}  module Photoname.Opts    ( parseOpts@@ -6,16 +6,15 @@    where  import Data.Version (showVersion)-import Formatting ((%), (%+), formatToString, string)+import Formatting ((%), (%+), format, formatToString, string) import Options.Applicative import Paths_photoname (version)+import Prettyprinter (pretty) import System.Directory (doesFileExist) import System.Environment (getArgs, getProgName) import System.Exit (exitFailure) import System.IO (hPutStrLn, stderr)-import System.Log (Priority (NOTICE)) import Text.Heredoc (here)-import Text.PrettyPrint.ANSI.Leijen qualified as Leijen  import Photoname.Common   ( Artist (..)@@ -29,6 +28,7 @@   , Options (..)   , ParentDir (..)   , Prefix (..)+  , Severity (..)   , Suffix (..)   , Verbosity (Verbose)   , defaultDateTimeFormat@@ -129,7 +129,7 @@         <> metavar "NUM"         <> help "Verbosity level. 0=quiet, 1=normal messages, 2=more info, 3=debug"         <> showDefault-        <> value (Verbose NOTICE)+        <> value (Verbose Notice)         )   <*> some ( strArgument         $ metavar "FILES..."@@ -144,7 +144,7 @@    confExists <- doesFileExist path     if confExists-      then map ("--" ++) . lines <$> readFile path+      then map ("--" <>) . lines <$> readFile path       else do         hPutStrLn stderr $ "Config file " <> path <> " does not exist!"         exitFailure@@ -185,8 +185,8 @@   footer' :: InfoMod a-footer' = footerDoc . Just . Leijen.string-  $ formatToString content defaultDateTimeFormat (showVersion version)+footer' = footerDoc . Just . pretty+  $ format content defaultDateTimeFormat (showVersion version)   where content = [here|OVERVIEW  This software is for renaming and storing your digital photos. It will attempt to construct a meaningful filename based on the EXIF shoot date in the file or possibly date/time info in the old filename, and optionally some other information.
src/app/photoname.hs view
@@ -1,20 +1,28 @@-{-# LANGUAGE OverloadedRecordDot, OverloadedStrings #-}+{-# LANGUAGE OverloadedRecordDot #-} -import Control.Monad (filterM, forM_, when)-import Formatting ((%), (%+), formatToString, string)+import Colog.Simple (logNotice)+-- import Colog.Simple (logTest)+import Control.Monad (filterM, when)+import Formatting ((%), sformat, string) import Data.Functor ((<&>))+import System.IO+  ( BufferMode (NoBuffering)+  , hSetBuffering+  , stderr, stdout+  ) import System.Posix (FileStatus, getFileStatus, isRegularFile)  import Photoname.Common (CopySwitch (v),   Extension (Extension, UseExistingExtension), Links, MoveSwitch (v),   NoActionSwitch (v), Options (copy, extension, links, move, noAction, paths,-  verbosity), Ph, SrcPath (SrcPath, v), liftIO, runRename)+  verbosity), SrcPath (SrcPath)) import Photoname.CopyLink (createNewLink) import Photoname.Date (PhDate, parseExifDate, parseFilenameDate) import Photoname.Exif (getExifDate) import Photoname.Exiv2 (getExifDateWithExiv2, setArtist, setExifDate) import Photoname.Links (describeHardLinkPolicy, linksTest)-import Photoname.Log (errorM, initLogging, lname, noticeM)+import Photoname.Log (logger)+import Photoname.Monad (Env (..), Ph, liftIO, runRename) import Photoname.Opts (parseOpts)  @@ -29,11 +37,14 @@  processFile :: SrcPath -> Ph () processFile srcPath = do-  liftIO . noticeM lname $ "----------"+  logNotice "------------------------------------------------------------"   imageDate <- acquireDate srcPath-  destPath <- createNewLink imageDate srcPath-  setExifDate imageDate destPath-  setArtist destPath+  mbDestPath <- createNewLink imageDate srcPath+  case mbDestPath of+    (Just destPath) -> do+      setExifDate imageDate destPath+      setArtist destPath+    Nothing -> pure ()   -- Get rid of anything not a regular file or with a number of links that@@ -51,33 +62,27 @@ -- Figure out and execute what the user wants based on the supplied args. main :: IO () main = do-   opts <- parseOpts--   initLogging $ verbosity opts--   -- Notify user of the switches that will be in effect.-   when opts.noAction.v $-      noticeM lname "No-action mode, nothing will be changed"+  mapM_ (flip hSetBuffering NoBuffering) [stderr, stdout]+  opts <- parseOpts+  runRename (Env opts (logger . verbosity $ opts)) $ do+    -- Notify user of the switches that will be in effect.+    when opts.noAction.v $+      logNotice "No-action mode, nothing will be changed" -   when opts.copy.v $-      noticeM lname "Files will be copied instead of the default of hard linking"+    when opts.copy.v $+      logNotice "Files will be copied instead of the default of hard linking" -   case opts.extension of-      Extension ext -> noticeM lname $ formatToString ("The extension '" % string % "' will be used for all files") ext+    case opts.extension of+      Extension ext -> logNotice $ sformat ("The extension '" % string % "' will be used for all files") ext       UseExistingExtension -> pure () -   when opts.move.v $-      noticeM lname "Removing original links after new links are in place"+    when opts.move.v $+      logNotice "Removing original links after new links are in place" -   describeHardLinkPolicy $ links opts+    describeHardLinkPolicy . links $ opts -   actualPaths <- filterWantedFiles (links opts) (paths opts)+    actualPaths <- liftIO $ filterWantedFiles (links opts) (paths opts) -   -- Do the link manipulations, and report any errors.-   forM_ actualPaths $ \srcPath -> do-      result <- runRename opts $ processFile srcPath-      either-        (errorM lname . formatToString ("** Processing" %+ string % ":" %+ string) srcPath.v)-        pure result+    mapM_ processFile actualPaths -   -- Perhaps we should get an ExitCode back from all this above?+    -- logTest
src/lib/Photoname/Common.hs view
@@ -12,50 +12,42 @@   , NoDirsSwitch (..)   , ParentDir (..)   , Options (..)-  , Ph   , Prefix (..)+  , Severity (..)   , SrcPath (..)   , Suffix (..)   , Verbosity (..)   , defaultDateTimeFormat   , readVerbosity-  , runRename--  -- Re-exporting:-  , MonadError-  , ask, asks-  , liftIO-  , throwError   )   where -import Control.Monad.Except (ExceptT, MonadError, runExceptT, throwError)-import Control.Monad.Reader (ReaderT, ask, asks, runReaderT)-import Control.Monad.Trans (liftIO)+import Colog.Simple (Severity (Debug, Info, Notice)) import Data.Time.LocalTime (LocalTime)-import System.Log.Logger (Priority (..)) import System.Posix (CNlink)   defaultDateTimeFormat :: String defaultDateTimeFormat = "%Y%m%d-%H%M%S" + data Verbosity   = Quiet-  | Verbose Priority+  | Verbose Severity  instance Show Verbosity where   show Quiet = "0"-  show (Verbose NOTICE) = "1"-  show (Verbose INFO) = "2"-  show (Verbose DEBUG) = "3"+  show (Verbose Notice) = "1"+  show (Verbose Info) = "2"+  show (Verbose Debug) = "3"   show _ = "Should never see this, invalid verbosity level being shown" + readVerbosity :: String -> Either String Verbosity readVerbosity "0" = Right   Quiet-readVerbosity "1" = Right $ Verbose NOTICE-readVerbosity "2" = Right $ Verbose INFO-readVerbosity "3" = Right $ Verbose DEBUG+readVerbosity "1" = Right $ Verbose Notice+readVerbosity "2" = Right $ Verbose Info+readVerbosity "3" = Right $ Verbose Debug readVerbosity _   = Left    "Invalid verbosity level, expecting 0-3"  @@ -104,10 +96,3 @@ newtype SrcPath = SrcPath { v :: FilePath }  newtype DestPath = DestPath FilePath---type Ph a = ReaderT Options (ExceptT String IO) a---runRename :: Options -> Ph a -> IO (Either String a)-runRename env action = runExceptT $ runReaderT action env
src/lib/Photoname/CopyLink.hs view
@@ -1,13 +1,16 @@-{-# LANGUAGE OverloadedRecordDot, ScopedTypeVariables #-}+{-# LANGUAGE OverloadedRecordDot #-}  module Photoname.CopyLink    ( createNewLink    )    where +import Colog.Simple (logNotice, logWarning) import Control.Exception (try) import Control.Monad (unless, when) import Data.Time.LocalTime (LocalTime)+import Formatting ((%+), sformat, string)+ import GHC.IO.Exception (IOException) import System.Directory (copyFile, createDirectoryIfMissing) import System.FilePath ((</>), (<.>), takeDirectory, takeExtension)@@ -17,47 +20,57 @@   Extension (Extension, UseExistingExtension), MoveSwitch (v),   NoActionSwitch (v), NoDirsSwitch (NoDirsSwitch), ParentDir (ParentDir),   Options (copy, formatter, extension, move, noAction, noDirs, parentDir, prefix, suffix),-  Ph, Prefix (Prefix), SrcPath (SrcPath), Suffix (Suffix), ask, asks, liftIO,-  throwError)+  Prefix (Prefix), SrcPath (SrcPath), Suffix (Suffix)) import Photoname.Date (PhDate (ExifDate, FilenameDate, NoDateFound),   formatDateHyphens, formatYear)-import Photoname.Log (lname, noticeM, warningM)+import Photoname.Monad (Env (envOptions), Ph, asks, liftIO)  -createNewLink :: PhDate -> SrcPath -> Ph DestPath-createNewLink imageDate srcPath@(SrcPath srcFp) = do-  opts <- ask+createNewLink :: PhDate -> SrcPath -> Ph (Maybe DestPath) +createNewLink NoDateFound (SrcPath srcFp) = do+  logWarning $ sformat ("Could not extract any date information from" %+ string) srcFp+  pure Nothing++createNewLink (ExifDate lt) srcPath = createNewLink' srcPath lt++createNewLink (FilenameDate lt) srcPath = createNewLink' srcPath lt+++createNewLink' :: SrcPath -> LocalTime -> Ph (Maybe DestPath)+createNewLink' srcPath@(SrcPath srcFp) lt = do+  opts <- asks envOptions+   let ext = case opts.extension of         (Extension ext') -> ext'         UseExistingExtension -> takeExtension srcFp -  destPath@(DestPath destFp) <- case imageDate of-    ExifDate lt -> buildDatePath lt ext-    FilenameDate lt -> buildDatePath lt ext-    NoDateFound -> throwError "Could not extract any date information"+  destPath@(DestPath destFp) <- buildDatePath lt ext    -- Check for existence of the target file-  exists <- liftIO $ fileExist destFp-  when exists $ throwError $ "Destination " ++ destFp ++ " exists!"--  -- Display what will be done-  liftIO $ noticeM lname $ srcFp ++ " -> " ++ destFp+  targetExists <- liftIO $ fileExist destFp+  if targetExists+    then do+      logWarning $ sformat ("Destination" %+ string %+ "exists!") destFp+      pure Nothing+    else do+      -- Display what will be done+      logNotice $ sformat (string %+ "->" %+ string) srcFp destFp -  unless opts.noAction.v $ do-    -- Make the target dir-    liftIO $ createDirectoryIfMissing True $ takeDirectory destFp+      unless opts.noAction.v $ do+        -- Make the target dir+        liftIO $ createDirectoryIfMissing True $ takeDirectory destFp -    -- Make the new file-    if opts.copy.v-      then liftIO $ copyFile srcFp destFp-      else tryHardLink srcPath destPath+        -- Make the new file+        if opts.copy.v+          then liftIO $ copyFile srcFp destFp+          else tryHardLink srcPath destPath -    -- If user has specified, remove the original link-    when opts.move.v $-       liftIO $ removeLink srcFp+        -- If user has specified, remove the original link+        when opts.move.v $+           liftIO $ removeLink srcFp -  pure destPath+      pure $ Just destPath   tryHardLink :: SrcPath -> DestPath -> Ph ()@@ -67,7 +80,7 @@   where     failureHandler :: IOException -> Ph ()     failureHandler _ = do-      liftIO $ warningM lname "Hard link failed, attempting to copy instead"+      logWarning "Hard link failed, attempting to copy instead"       liftIO $ copyFile srcFp destFp  @@ -77,13 +90,13 @@ -} buildDatePath :: LocalTime -> FilePath -> Ph DestPath buildDatePath date ext = do-   (Prefix prefixStr) <- asks prefix-   (Suffix suffixStr) <- asks suffix-   dateFormatter <- asks formatter+   (Prefix prefixStr) <- asks $ prefix . envOptions+   (Suffix suffixStr) <- asks $ suffix . envOptions+   dateFormatter <- asks $ formatter . envOptions    let fileName = prefixStr <> dateFormatter date <> suffixStr -   (ParentDir parentDir') <- asks parentDir-   (NoDirsSwitch noDirs') <- asks noDirs+   (ParentDir parentDir') <- asks $ parentDir . envOptions+   (NoDirsSwitch noDirs') <- asks $ noDirs . envOptions    pure . DestPath $ if noDirs'       then parentDir' </> fileName <.> ext       else parentDir' </> formatYear date </>
src/lib/Photoname/Date.hs view
@@ -1,5 +1,4 @@ {-# OPTIONS_GHC -fno-warn-unused-do-bind #-}-{-# LANGUAGE FlexibleContexts #-}  module Photoname.Date   ( PhDate (..)
src/lib/Photoname/Exif.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE FlexibleContexts #-}- module Photoname.Exif   ( getExifDate   )
src/lib/Photoname/Exiv2.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE OverloadedStrings #-}- module Photoname.Exiv2   ( getExifDateWithExiv2   , setArtist@@ -7,23 +5,27 @@   )   where +import Colog.Simple (logDebug, logInfo, logNotice) import Control.Exception import Control.Monad (void)-import Control.Monad.IO.Class (MonadIO) import Data.Char (isSpace) import Data.Monoid (First (..))-import Formatting ((%), (%+), formatToString, string)+import Data.Text (Text)+import Data.Text qualified as TS+import Formatting ((%), (%+), formatToString, string, text) import GHC.IO.Exception import System.Process hiding (proc) import qualified System.Process as Proc  import Photoname.Common (Artist (Artist), DestPath (DestPath),-  NoActionSwitch (NoActionSwitch), Options (artist, noAction), Ph,-  SrcPath (SrcPath), asks, liftIO)+  NoActionSwitch (NoActionSwitch), Options (artist, noAction),+  SrcPath (SrcPath)) import Photoname.Date (PhDate (FilenameDate), formatDateForExif)-import Photoname.Log (LogFunction, debugM, infoM, lname, noticeM)+import Photoname.Monad (Env (envOptions), Ph, asks, liftIO)  +type LogFunction = Text -> Ph ()+ data Reading data Writing @@ -37,9 +39,9 @@ -- System.Process.proc BUT they look odd when logged by our code. This function -- will put quotes around any space-containing arguments purely for human -- readability.-commandToString :: Command rw -> String-commandToString (Command _ program' arguments) =-  unwords $ program' : map quoteAsNeeded arguments+commandToText :: Command rw -> Text+commandToText (Command _ program' arguments) =+  TS.pack . unwords $ program' : map quoteAsNeeded arguments   where     quoteAsNeeded str = if ' ' `elem` str       then "'" <> str <> "'"@@ -49,25 +51,24 @@ proc (Command _ program' arguments) = Proc.proc program' arguments  -logCommand :: MonadIO m => Command rw -> m ()+logCommand :: Command rw -> Ph () logCommand command@(Command logFunction _ _) =-  liftIO . logFunction lname . commandToString $ command-+  logFunction . commandToText $ command   -- For Writing (or "destructive") commands, we need to check if the user has -- chosen no-action behavior before executing-execWritingCommand :: Command Writing -> Ph (Either String String)+execWritingCommand :: Command Writing -> Ph (Maybe String) execWritingCommand command = do   logCommand command-  (NoActionSwitch noAction') <- asks noAction+  (NoActionSwitch noAction') <- asks $ noAction . envOptions   if noAction'-    then pure $ Left ""+    then pure Nothing     else execCommand command   -- For Reading (or "non-destructive") commands, we just log it and do it-execReadingCommand :: Command Reading -> Ph (Either String String)+execReadingCommand :: Command Reading -> Ph (Maybe String) execReadingCommand command = logCommand command >> execCommand command  @@ -75,12 +76,19 @@ stripTrailingWhitespace = reverse . dropWhile isSpace . reverse  -execCommand :: Command rw -> Ph (Either String String)-execCommand command = liftIO $ do-  eResult <- postProcess =<< try (readCreateProcessWithExitCode (proc command) "")-  either (\msg -> debugM lname $ "** Command failed: " <> stripTrailingWhitespace msg)-    (\output -> debugM lname $ "Command succeeded, output: " <> stripTrailingWhitespace output) eResult-  pure eResult+execCommand :: Command rw -> Ph (Maybe String)+execCommand command = do+  eResult <- liftIO $ postProcess =<< try (readCreateProcessWithExitCode (proc command) "")+  either handleFailure handleSuccess eResult+  where+    handleFailure msg = do+      let msg' = formatToString (text %+ string) "** Command failed:" (stripTrailingWhitespace msg)+      logDebug . TS.pack $ msg'+      pure Nothing+    handleSuccess output = do+      let output' = formatToString (text %+ string) "Command succeeded, output:" (stripTrailingWhitespace output)+      logDebug . TS.pack $ output'+      pure . Just $ output'   program :: FilePath@@ -97,14 +105,14 @@  setArtist :: DestPath -> Ph () setArtist (DestPath destFp) = do-  artist' <- asks artist+  artist' <- asks $ artist . envOptions    case artist' of     Nothing -> pure ()     Just (Artist "") -> void $ execWritingCommand $-      Command noticeM program ["--Modify", "del Exif.Image.Artist", destFp]+      Command logNotice program ["--Modify", "del Exif.Image.Artist", destFp]     Just (Artist artistInfo) -> void $ execWritingCommand $-      Command noticeM program ["--Modify", "set Exif.Image.Artist " <> artistInfo, destFp]+      Command logNotice program ["--Modify", "set Exif.Image.Artist " <> artistInfo, destFp]   getExifDateWithExiv2 :: SrcPath -> Ph (Maybe String)@@ -118,15 +126,14 @@   ["Exif.Photo.DateTimeOriginal", "Exif.Photo.DateTimeDigitized", "Exif.Image.DateTime"]    where-    mbResult tag = do-      eResult <- execReadingCommand $ Command infoM program ["--Print", "v", "--grep", tag, srcFp]-      pure . either (const Nothing) Just $ eResult+    mbResult tag =+      execReadingCommand $ Command logInfo program ["--Print", "v", "--grep", tag, srcFp]   setExifDate :: PhDate -> DestPath -> Ph ()  setExifDate (FilenameDate lt) (DestPath destFp) =-  void $ execWritingCommand . Command noticeM program $+  void $ execWritingCommand . Command logNotice program $     [ "--Modify", "set Exif.Image.DateTime Ascii " <> formatDateForExif lt     , "--Modify", "set Exif.Photo.UserComment charset=Ascii DateTime is a guess", destFp     ]
src/lib/Photoname/Links.hs view
@@ -1,16 +1,15 @@-{-# LANGUAGE OverloadedStrings #-}- module Photoname.Links   ( describeHardLinkPolicy   , linksTest   )   where -import Formatting ((%+), formatToString, int)+import Colog.Simple (logNotice)+import Formatting ((%+), sformat, int) import System.Posix (FileStatus, linkCount)  import Photoname.Common (Links (Exactly, NoLimit))-import Photoname.Log (lname, noticeM)+import Photoname.Monad (Ph)   linksTest :: Links -> FileStatus -> Bool@@ -18,10 +17,9 @@ linksTest NoLimit                   _          = True  -describeHardLinkPolicy :: Links -> IO ()+describeHardLinkPolicy :: Links -> Ph () describeHardLinkPolicy l = case l of-  Exactly 1 -> noticeM lname "Only processing files with 1 hard link"-  Exactly n -> noticeM lname $ formatToString+  Exactly 1 -> logNotice "Only processing files with 1 hard link"+  Exactly n -> logNotice $ sformat     ("Only processing files with" %+ int %+ "hard links") (toInteger n)   NoLimit   -> pure ()-
src/lib/Photoname/Log.hs view
@@ -1,64 +1,18 @@ module Photoname.Log-  ( LogFunction-  , initLogging-  , lname-  , logTest--  -- Re-exported from System.Log-  , debugM, infoM, noticeM, warningM, errorM, criticalM, alertM, emergencyM+  ( logger   )   where -import Data.Functor ((<&>))-import System.IO (Handle, stdout)-import System.Log.Formatter (simpleLogFormatter)-import System.Log.Handler (setFormatter)-import System.Log.Handler.Simple (GenericHandler, streamHandler)-import System.Log.Logger-  ( Priority (DEBUG)-  , alertM, criticalM, debugM, emergencyM, errorM, infoM, noticeM, warningM-  , rootLoggerName, setHandlers, setLevel, updateGlobalLogger-  )--import Photoname.Common (Verbosity (Quiet, Verbose))---type LogFunction = String -> String -> IO ()---lname :: String-lname = rootLoggerName---initLogging :: Verbosity -> IO ()-initLogging verbosity = do-  updateGlobalLogger lname . setHandlers =<< handlers verbosity-  case verbosity of-    Quiet -> pure ()-    Verbose loggerLevel -> updateGlobalLogger lname $ setLevel loggerLevel---handlers :: Verbosity -> IO [GenericHandler Handle]--handlers Quiet  = pure []---- Under the maximum verbosity (-v3), also display the logging Priority-handlers (Verbose DEBUG) = do-  h <- streamHandler stdout DEBUG <&>-    flip setFormatter (simpleLogFormatter "$prio: $msg")-  pure [h]+import Colog.Simple (LogAction, Message, Severity(Debug), (%), (%+), msg, msgc,+  sevc, stack, logTextStdout)+import Colog.Simple qualified+import Control.Monad.Trans (MonadIO) -handlers (Verbose _) = sequence [streamHandler stdout DEBUG]+import Photoname.Common (Verbosity (..))  --- Test function to generate every kind of log message-logTest :: IO ()-logTest = do-  debugM     lname "log test message DEBUG 1 of 8"-  infoM      lname "log test message INFO 2 of 8"-  noticeM    lname "log test message NOTICE 3 of 8"-  warningM   lname "log test message WARNING 4 of 8"-  errorM     lname "log test message ERROR 5 of 8"-  criticalM  lname "log test message CRITICAL 6 of 8"-  alertM     lname "log test message ALERT 7 of 8"-  emergencyM lname "log test message EMERGENCY 8 of 8"+-- Wrapper to set up logging based on desired verbosity+logger :: MonadIO m => Verbosity -> LogAction m Message+logger  Quiet          = mempty+logger (Verbose Debug) = Colog.Simple.logger (sevc % stack %+ msg) logTextStdout Debug+logger (Verbose sev)   = Colog.Simple.logger msgc logTextStdout sev
+ src/lib/Photoname/Monad.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE DerivingStrategies #-}++module Photoname.Monad+  ( Env (..)+  , Ph+  , runRename++  -- Re-exporting:+  , ask, asks+  , liftIO+  )+  where++import Colog.Simple (HasLog (..), LogAction, Message)+import Control.Monad.Reader (MonadReader, ReaderT, ask, asks, runReaderT)+import Control.Monad.Trans (MonadIO, liftIO)++import Photoname.Common (Options)+++data Env m = Env+  { envOptions :: !Options+  , envLogAction :: !(LogAction m Message)+  }++instance HasLog (Env m) Message m where+  getLogAction :: Env m -> LogAction m Message+  getLogAction = envLogAction+  {-# INLINE getLogAction #-}++  setLogAction :: LogAction m Message -> Env m -> Env m+  setLogAction newLogAction env = env { envLogAction = newLogAction }+  {-# INLINE setLogAction #-}+++newtype Ph a = Ph { unPh :: ReaderT (Env Ph) IO a }+  deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader (Env Ph))++runRename :: Env Ph -> Ph a -> IO a+runRename env action = runReaderT (unPh action) env
src/tests/Photoname/Test/EndToEnd/Link.hs view
@@ -46,7 +46,7 @@ testLinkDigitized = testCase "tests for DateTimeDigitized" $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, oldPath ]+      [ "--parent-dir=" <> parentDir, oldPath ]    waitForProcess procH     -- Check that the correct output path exists@@ -71,7 +71,7 @@     -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, digitizedOldPath ]+      [ "--parent-dir=" <> parentDir, digitizedOldPath ]    waitForProcess procH     -- Check that the correct output path exists@@ -97,7 +97,7 @@     -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, dateOldPath ]+      [ "--parent-dir=" <> parentDir, dateOldPath ]    waitForProcess procH     -- Check that the correct output path exists@@ -120,12 +120,12 @@ testNoDate = testCase "test for no date in the file" $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, Util.resourcesPath </> "noDate.jpg" ]+      [ "--parent-dir=" <> parentDir, Util.resourcesPath </> "noDate.jpg" ]    waitForProcess procH     -- Test output to stdout    assertBool "no EXIF: correct output"-      (output =~ "\\*\\* Processing util/resources/test/noDate.jpg: Could not extract any date information" :: Bool)+      (output =~ ("Could not extract any date information from util/resources/test/noDate.jpg" :: String) :: Bool)   testMove :: TestTree@@ -137,7 +137,7 @@     -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--move", "--parent-dir=" ++ parentDir, newOldPath ]+      [ "--move", "--parent-dir=" <> parentDir, newOldPath ]    waitForProcess procH     -- Check that the correct output path exists@@ -168,19 +168,19 @@ testLinkNoAction' label switch = testCase label $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ switch, "--parent-dir=" ++ parentDir, oldPath ]+      [ switch, "--parent-dir=" <> parentDir, oldPath ]    waitForProcess procH     -- Check that the correct output path exists    existsNew <- fileExist parentDir-   Util.assertFalse (label ++ ": existence of new link") existsNew+   Util.assertFalse (label <> ": existence of new link") existsNew     -- Check that old path still exists    existsOld <- fileExist oldPath-   assertBool (label ++ ": existence of old link") existsOld+   assertBool (label <> ": existence of old link") existsOld     -- Test output to stdout-   assertBool (label ++ ": correct output")+   assertBool (label <> ": correct output")       (output =~ newLinkPathDate :: Bool)  @@ -197,22 +197,22 @@ testLinkQuiet' label switch = testCase label $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ switch, "--parent-dir=" ++ parentDir, oldPath ]+      [ switch, "--parent-dir=" <> parentDir, oldPath ]    waitForProcess procH     -- Check that the correct output path exists    existsNew <- fileExist newLinkPathDate-   assertBool (label ++ ": existence of new link") existsNew+   assertBool (label <> ": existence of new link") existsNew     -- Check that old path still exists    existsOld <- fileExist oldPath-   assertBool (label ++ ": existence of old link") existsOld+   assertBool (label <> ": existence of old link") existsOld     -- Remove files and dirs that were created    removeDirectoryRecursive parentDir     -- Test output to stdout-   Util.assertFalse (label ++ ": no output")+   Util.assertFalse (label <> ": no output")       (output =~ newLinkPathDate :: Bool)  @@ -222,11 +222,11 @@     -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, "--suffix=" ++ suffix, oldPath ]+      [ "--parent-dir=" <> parentDir, "--suffix=" <> suffix, oldPath ]    waitForProcess procH     let newLinkPath = parentDir </>-         ("2003/2003-09-02/20030902-114303" ++ suffix) <.> "jpg"+         ("2003/2003-09-02/20030902-114303" <> suffix) <.> "jpg"     -- Check that the correct output path exists    existsNew <- fileExist newLinkPath@@ -250,7 +250,7 @@    -- Run the program with known input data   (output, procH) <- Util.getBinaryOutput-    [ "--parent-dir=" ++ parentDir, "--prefix=" ++ prefix, oldPath ]+    [ "--parent-dir=" <> parentDir, "--prefix=" <> prefix, oldPath ]   waitForProcess procH    let newLinkPath = parentDir </>@@ -276,36 +276,36 @@ testNoExif = testCase "test for a file without any EXIF data" $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, Util.resourcesPath </> "noExif.jpg" ]+      [ "--parent-dir=" <> parentDir, Util.resourcesPath </> "noExif.jpg" ]    waitForProcess procH     -- Test output to stdout    assertBool "no EXIF: correct output"-      (output =~ "\\*\\* Processing util/resources/test/noExif.jpg: Could not extract any date information" :: Bool)+      (output =~ ("Could not extract any date information from util/resources/test/noExif.jpg" :: String) :: Bool)   testNotAnImage :: TestTree testNotAnImage = testCase "test for a file that isn't an image" $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, Util.resourcesPath </> "notAnImage.txt" ]+      [ "--parent-dir=" <> parentDir, Util.resourcesPath </> "notAnImage.txt" ]    waitForProcess procH     -- Test output to stdout    assertBool "no EXIF: correct output"-      (output =~ "\\*\\* Processing util/resources/test/notAnImage.txt: Could not extract any date information" :: Bool)+      (output =~ ("Could not extract any date information from util/resources/test/notAnImage.txt" :: String) :: Bool)   testDirForFile :: TestTree testDirForFile = testCase "test when a directory is passed instead of a file" $ do    -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, Util.resourcesPath ]+      [ "--parent-dir=" <> parentDir, Util.resourcesPath ]    waitForProcess procH     -- Test output to stdout    assertBool "dir as file to change: correct output"-      (output =~ "" :: Bool)+      (output =~ ("" :: String) :: Bool)   testLinkFilenameDate :: TestTree@@ -318,7 +318,7 @@     -- Run the program with known input data    (output, procH) <- Util.getBinaryOutput-      [ "--parent-dir=" ++ parentDir, newOldPath ]+      [ "--parent-dir=" <> parentDir, newOldPath ]    waitForProcess procH     -- Check that the correct output path exists
stack.yaml view
@@ -1,10 +1,10 @@-resolver: lts-21.25+snapshot: lts-22.6  packages: - .  extra-deps:-- hsexif-0.6.1.10+- co-log-simple-1.1.0  flags:   hsexif:
stack.yaml.lock view
@@ -1,19 +1,19 @@ # This file was autogenerated by Stack. # You should not edit this file by hand. # For more information, please see the documentation at:-#   https://docs.haskellstack.org/en/stable/lock_files+#   https://docs.haskellstack.org/en/stable/topics/lock_files  packages: - completed:-    hackage: hsexif-0.6.1.10@sha256:f002993b7953186a9e13869efe830ce951b900ee504afdbc297950f3b88fc3e3,2276+    hackage: co-log-simple-1.1.0@sha256:8cc7c99d60d0c6088e76e2548f7621737ae7e26f1237c181ead009ee9763c951,1767     pantry-tree:-      sha256: 25066251bd371301fa2d8e693d5e84828ceee2a02e2ee7f1523065fc2cbf72db-      size: 1120+      sha256: 918c592895abf6aeb758343c9c9090b3c92ba20b31f2e24f53b7f62aa572b5a0+      size: 579   original:-    hackage: hsexif-0.6.1.10+    hackage: co-log-simple-1.1.0 snapshots: - completed:-    sha256: a81fb3877c4f9031e1325eb3935122e608d80715dc16b586eb11ddbff8671ecd-    size: 640086-    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/25.yaml-  original: lts-21.25+    sha256: 1b4c2669e26fa828451830ed4725e4d406acc25a1fa24fcc039465dd13d7a575+    size: 714100+    url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/6.yaml+  original: lts-22.6