hbro-contrib-1.5.0.0: Hbro/History.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
-- | Designed to be imported as @qualified@.
module Hbro.History
( Entry(..)
, HistoryException(..)
, log
, log'
, add
, add'
, Hbro.History.select
, Hbro.History.select'
) where
-- {{{ Imports
import Hbro
import Hbro.Logger
import Hbro.Misc
import Data.Aeson.Extended
import qualified Data.Set as Set
import Data.Time
import Network.URI
import System.Directory
-- }}}
-- {{{ Type definitions
data Entry = Entry
{ _time :: UTCTime
, _uri :: URI
, _title :: Text
}
deriving instance Eq Entry
instance Ord Entry where
compare (Entry t u _) (Entry t' u' _)
| u == u' = EQ
| otherwise = compare t t'
instance Describable Entry where
describe (Entry time uri title) = unwords [pack (formatTime defaultTimeLocale dateFormat time), tshow uri, title]
instance FromJSON Entry where
parseJSON (Object v) = Entry <$> v .: "time" <*> (unwrapURI <$> v .: "uri") <*> v .: "title"
parseJSON _ = mzero
instance ToJSON Entry where
toJSON (Entry time uri title) = object ["time" .= time, "uri" .= WrappedURI uri, "title" .= title]
data HistoryException = InvalidSelection deriving(Eq)
instance Exception HistoryException
instance Show HistoryException where
show InvalidSelection = "Invalid history item selected."
-- }}}
dateFormat :: String
dateFormat = "%F %T"
getHistoryFile :: (BaseIO m) => m FilePath
getHistoryFile = getAppUserDataDirectory "hbro" >/> "history"
-- | Log current visited page to history database
log :: (ControlIO m, MonadLogger m, MonadReader r m, Has MainView r, MonadThrow m, Alternative m) => m ()
log = log' =<< getHistoryFile
-- | Like 'log', but you can specify the history file path
log' :: (ControlIO m, MonadLogger m, MonadReader r m, Has MainView r, MonadThrow m, Alternative m) => FilePath -> m ()
log' file = do
uri <- getCurrentURI
title <- getPageTitle
now <- io $ zonedTimeToUTC <$> getZonedTime
add' file (Entry now uri title)
-- | Add a new entry to history database
add :: (ControlIO m, MonadLogger m, MonadThrow m, Alternative m) => Entry -> m ()
add newEntry = (`add'` newEntry) =<< getHistoryFile
-- | Like 'add', but you can specify the history file path
add' :: (ControlIO m, MonadLogger m, MonadThrow m, Alternative m) => FilePath -> Entry -> m ()
add' file newEntry = do
debug $ "Adding new entry <" ++ tshow (_uri newEntry) ++ "> to history file <" ++ pack file ++ ">"
tryIO . io . copyFile file $ file <.> "bak"
entries <- (decodeM =<< readFile file) <|> return Set.empty
writeFile file . encodePretty $ Set.insert newEntry entries
-- | Open a dmenu with all (sorted alphabetically) history entries, and return the user's selection, if any
select :: (ControlIO m, MonadThrow m) => m Entry
select = (`select'` defaultDmenuOptions) =<< getHistoryFile
-- | Like 'select', but you can specify the history file path
select' :: (ControlIO m, MonadThrow m) => FilePath -> [Text] -> m Entry
select' file dmenuOptions = do
entries <- zip [1..] . sortBy (flip (comparing _time)) <$> (decodeM =<< readFile file)
result <- dmenu dmenuOptions . unlines $ map (\(i :: Int, e :: Entry) -> tshow i ++ " " ++ describe e) entries
maybe (throwM InvalidSelection) return ((`lookup` entries) =<< readMay =<< headMay (words result))