suntheme/app/Main.hs

207 lines
6.7 KiB
Haskell

-- requires 'at' for running a command at a certain time
-- requires 'date' for converting unix time to human readable time
-- run on boot and at noon and midnight
-- don't add commands to at while this script is running (monadic purity must be preserved)
module Main where
import Const (darkModeScript, lightModeScript, prog, query)
import Types (ResponseMsg(..), ResponseCode(..), Status(..), genericErr, toResponseMsg)
import Data.Time (ZonedTime, getCurrentTime)
import Data.Time.Solar (Location(Location), sunrise, sunset)
import Data.Time.RFC3339 (formatTimeRFC3339)
import Data.Time.LocalTime (getTimeZone, utcToZonedTime, zonedTimeToUTC)
import Data.List.Extra ((!?))
import Data.ByteString.Char8 (unpack)
import System.Exit (ExitCode(ExitFailure), exitWith)
import System.Process (readProcess)
import System.FilePath ((</>), takeDirectory)
import System.Directory (XdgDirectory(XdgCache, XdgConfig), createDirectoryIfMissing, doesFileExist, getXdgDirectory)
import Control.Exception (SomeException, catch, try)
import Network.HTTP.Request (Response(responseBody, responseStatus), get)
-- error handling
-- refactor into modules
-- clean up jank (reduce lines in functions, more atomic/functional, etc)
-- eliminate as many do blocks as possible
-- introduce liquid types and checking
-- whitepaper!
now :: IO ZonedTime
now = do
utcTime <- getCurrentTime
timeZone <- getTimeZone utcTime
return (utcToZonedTime timeZone utcTime)
sunriseNow :: Double -> Double -> IO ZonedTime
sunriseNow lat lon = do
time <- now
return (sunrise time here)
where here = Location lat lon
sunsetNow :: Double -> Double -> IO ZonedTime
sunsetNow lat lon = do
time <- now
return (sunset time here)
where here = Location lat lon
pathToCache :: String -> IO String
pathToCache str = do
dir <- getXdgDirectory XdgCache prog
return (dir </> str)
pathToConfig :: String -> IO String
pathToConfig str = do
dir <- getXdgDirectory XdgConfig prog
return (dir </> str)
fetch :: String -> IO (Either SomeException Response)
fetch = try . get
cont :: (Status s) => s -> IO () -> IO ()
cont e err = (putStrLn . disp) e >> err
destruct :: (Status s) => s -> IO () -> IO () -> IO ()
destruct status success failure
| ok status = success
| otherwise = cont status failure
ping :: (Response -> IO ()) -> IO () -> IO ()
ping run err = do
res <- fetch query
case res of
Left e -> cont (toResponseMsg e) err
Right r ->
let status = (ResponseCode . responseStatus) r
runner = run r
in destruct status runner err
readLines :: [String] -> Maybe (String, Double, Double, String)
readLines [msg, latStr, lonStr, tz] = Just (msg, read latStr, read lonStr, tz)
readLines _ = Nothing
process :: Response -> (Double -> Double -> String -> IO ()) -> IO () -> IO ()
process r run err =
case info of
Nothing -> cont genericErr err
Just (msg, lat, lon, tz) ->
let status = ResponseMsg msg
runner = run lat lon tz
in destruct status runner err
where
body = responseBody r
parts = (lines . unpack) body
info = (readLines . take 4) parts
dumpCache :: Double -> Double -> String -> IO ()
dumpCache lat lon tz = do
cache <- pathToCache "data.txt"
catch (writer cache) failure
prepareScripts lat lon
where
writer dir = writeFile dir (show lat ++ "\n" ++ show lon ++ "\n" ++ tz)
failure = print :: SomeException -> IO ()
readCache :: IO (Maybe (Double, Double, String))
readCache = do
cache <- pathToCache "data.txt"
contents <- readFile cache
let entries = lines contents
case (entries !? 0, entries !? 1, entries !? 2) of
(Just lat, Just lon, Just tz) -> return (Just (read lat, read lon, tz))
_ -> return Nothing
processRunner :: Response -> IO ()
processRunner r = process r dumpCache backupRunner
crash :: IO ()
crash = exitWith (ExitFailure 1)
backupRunner :: IO ()
backupRunner = do
contents <- readCache
case contents of
Nothing -> do
putStrLn "Failed to read cache after IP location failed"
crash
Just (lat, lon, _) -> prepareScripts lat lon
exec :: String -> (SomeException -> IO String) -> IO String
exec cmd err = do catch (readProcess "bash" ["-c", cmd] "") err
kill :: String -> String
kill = (++) "atrm "
killall :: [String] -> IO ()
killall = foldr ((>>) . dispatch . kill) (return ())
where
dispatch cmd = exec cmd failure
failure :: SomeException -> IO String
failure e = print e >> return []
start :: IO () -> IO ()
start act = do
logFile <- pathToCache "log.txt"
existsLog <- doesFileExist logFile
if existsLog then do
contents <- readFile logFile
let entries = lines contents
killall entries
else createDirectoryIfMissing True (takeDirectory logFile)
act
finish :: String -> IO ()
finish queue = do
cache <- pathToCache "log.txt"
catch (writeFile cache num) failure
where
num = getId queue
getId = head . words . last . lines
failure = print :: SomeException -> IO ()
after :: (Eq a) => a -> [a] -> [a]
after c = drop 1 . dropWhile (/= c)
formatTime :: ZonedTime -> String
formatTime = take 5 . after 'T' . formatTimeRFC3339
buildCmd :: String -> ZonedTime -> String
buildCmd script time = "echo \"" ++ script ++ "\" | at " ++ formatTime time
activateOnSunrise :: ZonedTime -> ZonedTime -> IO Bool
activateOnSunrise sunriseTime sunsetTime = do
timeNow <- now
let utcTimeNow = zonedTimeToUTC timeNow
utcSunrise = zonedTimeToUTC sunriseTime
utcSunset = zonedTimeToUTC sunsetTime
if utcTimeNow < utcSunrise || utcTimeNow > utcSunset then return True
else return False
prepareScripts :: Double -> Double -> IO ()
prepareScripts lat lon = do
sunriseTime <- sunriseNow lat lon
sunsetTime <- sunsetNow lat lon
lightMode <- activateOnSunrise sunriseTime sunsetTime
_ <- if lightMode then do
putStr "Light mode activation script scheduled for "
print sunriseTime
script <- pathToConfig lightModeScript
exec (buildCmd script sunriseTime) terminate
else do
putStrLn "Dark mode activation script scheduled for "
print sunsetTime
script <- pathToConfig darkModeScript
exec (buildCmd script sunsetTime) terminate
queue <- exec "atq" noQueue
finish queue
where
terminate _ = print "Scheduling process failed" >> return ""
noQueueMsg = "Scheduled process could not be retrieved (try rerunning if 'atq' fails)"
noQueue _ = print noQueueMsg >> return ""
routine :: IO ()
routine = ping processRunner backupRunner
main :: IO ()
main = start routine