{-# LANGUAGE OverloadedStrings #-}
module Cursor
( Cursor,
newMem,
newFile,
get,
set,
pollLines,
pollFile,
pollNumberedFile,
seekEnd,
seekEndFile,
readLogLinesComplete,
)
where
import Data.Char (isSpace)
import Data.IORef
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as TIO
import System.Directory (doesFileExist)
import Text.Read (readMaybe)
import Prelude
data Cursor = Cursor
{ Cursor -> IO Int
cursorGet :: IO Int,
Cursor -> Int -> IO ()
cursorSet :: Int -> IO ()
}
newMem :: Int -> IO Cursor
newMem :: Int -> IO Cursor
newMem Int
n0 = do
ref <- Int -> IO (IORef Int)
forall a. a -> IO (IORef a)
newIORef Int
n0
pure
Cursor
{ cursorGet = readIORef ref,
cursorSet = writeIORef ref
}
newFile :: FilePath -> IO Cursor
newFile :: String -> IO Cursor
newFile String
path =
Cursor -> IO Cursor
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure
Cursor
{ cursorGet :: IO Int
cursorGet = String -> IO Int
readFilePos String
path,
cursorSet :: Int -> IO ()
cursorSet = String -> Int -> IO ()
writeFilePos String
path
}
get :: Cursor -> IO Int
get :: Cursor -> IO Int
get = Cursor -> IO Int
cursorGet
set :: Cursor -> Int -> IO ()
set :: Cursor -> Int -> IO ()
set = Cursor -> Int -> IO ()
cursorSet
pollLines :: Cursor -> [Text] -> IO [Text]
pollLines :: Cursor -> [Text] -> IO [Text]
pollLines Cursor
c [Text]
xs = do
pos <- Cursor -> IO Int
cursorGet Cursor
c
let total = [Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
xs
pos' = if Int
pos Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
total then Int
0 else Int
pos
news = Int -> [Text] -> [Text]
forall a. Int -> [a] -> [a]
drop Int
pos' [Text]
xs
cursorSet c total
pure news
pollFile :: Cursor -> FilePath -> IO [Text]
pollFile :: Cursor -> String -> IO [Text]
pollFile Cursor
c String
path = do
ls <- String -> IO [Text]
readLogLines String
path
pollLines c ls
pollNumberedFile :: Cursor -> FilePath -> IO [(Int, Text)]
pollNumberedFile :: Cursor -> String -> IO [(Int, Text)]
pollNumberedFile Cursor
c String
path = do
ls <- String -> IO [Text]
readLogLinesComplete String
path
pos <- get c
let total = [Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
ls
pos' = if Int
pos Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
total then Int
0 else Int
pos
news = Int -> [Text] -> [Text]
forall a. Int -> [a] -> [a]
drop Int
pos' [Text]
ls
set c total
pure (zip [pos' + 1 ..] news)
seekEnd :: Cursor -> [Text] -> IO ()
seekEnd :: Cursor -> [Text] -> IO ()
seekEnd Cursor
c [Text]
xs = Cursor -> Int -> IO ()
cursorSet Cursor
c ([Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
xs)
seekEndFile :: Cursor -> FilePath -> IO ()
seekEndFile :: Cursor -> String -> IO ()
seekEndFile Cursor
c String
path = do
ls <- String -> IO [Text]
readLogLines String
path
seekEnd c ls
readFilePos :: FilePath -> IO Int
readFilePos :: String -> IO Int
readFilePos String
path = do
exists <- String -> IO Bool
doesFileExist String
path
if not exists
then pure 0
else do
raw <- T.unpack <$> TIO.readFile path
pure $ fromMaybe 0 $ readMaybe (filter (not . isSpace) raw)
writeFilePos :: FilePath -> Int -> IO ()
writeFilePos :: String -> Int -> IO ()
writeFilePos String
path Int
n = String -> Text -> IO ()
TIO.writeFile String
path (String -> Text
T.pack (Int -> String
forall a. Show a => a -> String
show Int
n String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
"\n"))
readLogLines :: FilePath -> IO [Text]
readLogLines :: String -> IO [Text]
readLogLines String
path = do
exists <- String -> IO Bool
doesFileExist String
path
if not exists
then pure []
else do
content <- TIO.readFile path
pure $ if T.null content then [] else T.lines content
readLogLinesComplete :: FilePath -> IO [Text]
readLogLinesComplete :: String -> IO [Text]
readLogLinesComplete String
path = do
exists <- String -> IO Bool
doesFileExist String
path
if not exists
then pure []
else do
content <- TIO.readFile path
let ls = if Text -> Bool
T.null Text
content then [] else Text -> [Text]
T.lines Text
content
pure $
if T.isSuffixOf "\n" content
then ls
else take (length ls - 1) ls