emanote-1.2.0.0: src/Emanote/Model/Stork/Index.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TemplateHaskell #-}
module Emanote.Model.Stork.Index (
IndexVar,
newIndex,
clearStorkIndex,
readOrBuildStorkIndex,
File (File),
Input (Input),
Config (Config),
Handling,
FileType (..),
) where
import Control.Monad.Logger (MonadLoggerIO)
import Data.Default (Default (..))
import Data.Text qualified as T
import Data.Time (NominalDiffTime, diffUTCTime, getCurrentTime)
import Deriving.Aeson
import Emanote.Prelude (log, logD, logW)
import Numeric (showGFloat)
import Relude
import System.Process.ByteString (readProcessWithExitCode)
import System.Which (staticWhich)
import Toml (Key, TomlCodec, diwrap, encode, list, string, table, text, textBy, (.=))
-- | In-memory Stork index tracked in a @TVar@
newtype IndexVar = IndexVar (TVar (Maybe LByteString))
newIndex :: (MonadIO m) => m IndexVar
newIndex =
IndexVar <$> newTVarIO mempty
clearStorkIndex :: (MonadIO m) => IndexVar -> m ()
clearStorkIndex (IndexVar var) = atomically $ writeTVar var mempty
readOrBuildStorkIndex :: (MonadIO m, MonadLoggerIO m) => IndexVar -> Config -> m LByteString
readOrBuildStorkIndex (IndexVar indexVar) config = do
readTVarIO indexVar >>= \case
Just index -> do
logD "STORK: Returning cached search index"
pure index
Nothing -> do
-- TODO: What if there are concurrent reads? We probably need a lock.
-- And we want to encapsulate this whole thing.
logW "STORK: Generating search index (this may be expensive)"
(diff, !index) <- timeIt $ runStork config
log $ toText $ "STORK: Done generating search index in " <> showGFloat (Just 2) diff "" <> " seconds"
atomically $ modifyTVar' indexVar $ \_ -> Just index
pure index
where
timeIt :: (MonadIO m) => m b -> m (Double, b)
timeIt m = do
t0 <- liftIO getCurrentTime
!x <- m
t1 <- liftIO getCurrentTime
let diff :: NominalDiffTime = diffUTCTime t1 t0
pure (realToFrac diff, x)
storkBin :: FilePath
storkBin = $(staticWhich "stork")
runStork :: (MonadIO m) => Config -> m LByteString
runStork config = do
let storkToml = handleTomlandBug $ Toml.encode configCodec config
(_, !index, _) <-
liftIO $
readProcessWithExitCode
storkBin
-- NOTE: Cannot use "--output -" due to bug in Rust or Stork:
-- https://github.com/jameslittle230/stork/issues/262
["build", "-t", "--input", "-", "--output", "/dev/stdout"]
(encodeUtf8 storkToml)
pure $ toLazy index
where
handleTomlandBug =
-- HACK: Deal with tomland's bug.
-- https://github.com/srid/emanote/issues/336
-- https://github.com/kowainik/tomland/issues/408
--
-- This could be problematic if the user literally uses \\U in their note
-- title (but why would they?)
T.replace "\\\\U" "\\U"
newtype Config = Config
{ configInput :: Input
}
deriving stock (Eq, Show)
data Input = Input
{ inputFiles :: [File]
, inputFrontmatterHandling :: Handling
}
deriving stock (Eq, Show)
data File = File
{ filePath :: FilePath
, fileUrl :: Text
, fileTitle :: Text
, fileFiletype :: FileType
}
deriving stock (Eq, Show)
data FileType
= FileType_PlainText
| FileType_Markdown
deriving stock (Eq, Show, Generic)
deriving
(FromJSON)
via CustomJSON
'[ ConstructorTagModifier '[StripPrefix "FileType_", CamelToSnake]
]
FileType
data Handling
= Handling_Ignore
| Handling_Omit
| Handling_Parse
deriving stock (Eq, Show, Generic)
deriving
(FromJSON)
via CustomJSON
'[ ConstructorTagModifier '[StripPrefix "Handling_", CamelToSnake]
]
Handling
instance Default Handling where
def = Handling_Omit
configCodec :: TomlCodec Config
configCodec =
Config
<$> Toml.table inputCodec "input"
.= configInput
where
inputCodec :: TomlCodec Input
inputCodec =
Input
<$> Toml.list fileCodec "files"
.= inputFiles
<*> Toml.diwrap (handlingCodec "frontmatter_handling")
.= inputFrontmatterHandling
fileCodec :: TomlCodec File
fileCodec =
File
<$> Toml.string "path"
.= filePath
<*> Toml.text "url"
.= fileUrl
<*> Toml.text "title"
.= fileTitle
<*> Toml.diwrap (filetypeCodec "filetype")
.= fileFiletype
handlingCodec :: Toml.Key -> TomlCodec Handling
handlingCodec = textBy showHandling parseHandling
where
showHandling :: Handling -> Text
showHandling handling = case handling of
Handling_Ignore -> "Ignore"
Handling_Omit -> "Omit"
Handling_Parse -> "Parse"
parseHandling :: Text -> Either Text Handling
parseHandling handling = case handling of
"Ignore" -> Right Handling_Ignore
"Omit" -> Right Handling_Omit
"Parse" -> Right Handling_Parse
other -> Left $ "Unsupported value for frontmatter handling: " <> other
filetypeCodec :: Toml.Key -> TomlCodec FileType
filetypeCodec = textBy showFileType parseFileType
where
showFileType :: FileType -> Text
showFileType filetype = case filetype of
FileType_PlainText -> "PlainText"
FileType_Markdown -> "Markdown"
parseFileType :: Text -> Either Text FileType
parseFileType filetype = case filetype of
"PlainText" -> Right FileType_PlainText
"Markdown" -> Right FileType_Markdown
other -> Left $ "Unsupported value for filetype: " <> other