diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..09118b1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ +name: "Cache binaries" +on: + pull_request: + push: +jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v25 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v14 + with: + name: suntheme + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + - run: nix build --accept-flake-config + - run: nix develop --accept-flake-config diff --git a/.gitignore b/.gitignore index 4c9e245..f512922 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ cabal.project.local cabal.project.local~ .HTF/ .ghc.environment.* +result +.direnv/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdfec74 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Suntheme + +Runs a script on sunrise and sunset, written in pure Haskell + +--- + +You may be wondering how a program written in Haskell, the purely functional +programming language, could possibly act on the real world by running a so-called "script." + +It's simple. We take in the entire World as an input to a pure function, the IO Monad. +It then maps the original World to a new (generated) World, with our desired IO actions carefully applied with mathematical precision. + +> From the second perspective, an IO action transforms the whole world. IO actions are actually pure, because they receive a unique world as an argument and then return the changed world. + +See [this](https://lean-lang.org/functional_programming_in_lean/monads/io.html) for more information. + +## Hacking on suntheme + +It's trivial to get started with suntheme development thanks to [Nix](https://nixos.org/), the purely functional package manager. +Naturally, we leverage it as our primary package manager, both for Hackage and development tools like language servers and the like. + +First, install Nix through your preferred avenue or local system administrator. If unsure, we recommend [the Determinate Nix Installer](https://github.com/DeterminateSystems/nix-installer). +Make sure flakes and nix-command are enabled (the Determinate Installer will enable them by default). + +Once you have `nix`, simply type: + +```bash +nix develop +``` + +Say yes to any prompts asking you to allow substituters or trust public keys. +Nix will fetch all of the required packages, such as GHC and Hackage dependencies. +Additionally, you will have access to `hlint` and the `haskell-language-server`. + +To create a build, type + +```bash +nix build +``` + +A binary will be produced in `result/bin/suntheme`. + +To make `suntheme` available in the shell without outputting to `result`, use + +```bash +nix shell +``` + +This will build a binary just like `nix build` but add it temporarily to the PATH, so you can just type `suntheme`. + +To build and run `suntheme` immediately without adding it to the PATH, use + +```bash +nix run +``` + +This will build a binary just like `nix shell` but immediately execute it. diff --git a/app/Cache.hs b/app/Cache.hs new file mode 100644 index 0000000..ef0ebef --- /dev/null +++ b/app/Cache.hs @@ -0,0 +1,51 @@ +module Cache where + +import Const (cacheFile, logFile, prog) +import Sugar (killall, failure) + +import Data.List.Extra ((!?)) +import System.FilePath ((), takeDirectory) +import System.Directory ( + XdgDirectory(XdgCache, XdgConfig), + createDirectoryIfMissing, doesFileExist, getXdgDirectory + ) +import Control.Exception (catch) + +pathToCache :: String -> IO String +pathToCache str = ( str) <$> getXdgDirectory XdgCache prog + +pathToConfig :: String -> IO String +pathToConfig str = ( str) <$> getXdgDirectory XdgConfig prog + +readCache :: IO (Maybe (Double, Double, String)) +readCache = do + cache <- pathToCache cacheFile + 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 + +dumpCache :: Double -> Double -> String -> IO (Double, Double) +dumpCache lat lon tz = do + cache <- pathToCache cacheFile + catch (writer cache) failure + return (lat, lon) + where writer dir = writeFile dir (show lat ++ "\n" ++ show lon ++ "\n" ++ tz) + +start :: IO () +start = do + logs <- pathToCache logFile + existsLog <- doesFileExist logs + if existsLog then do + contents <- readFile logs + (sequence_ . killall . lines) contents + else createDirectoryIfMissing True (takeDirectory logs) + +finish :: String -> IO () +finish queue = do + cache <- pathToCache logFile + catch (writeFile cache num) failure + where + getId = head . words . last . lines + num = getId queue diff --git a/app/Const.hs b/app/Const.hs new file mode 100644 index 0000000..3af5223 --- /dev/null +++ b/app/Const.hs @@ -0,0 +1,19 @@ +module Const where + +query :: String +query = "http://ip-api.com/line/?fields=status,lat,lon,timezone" + +prog :: String +prog = "suntheme" + +cacheFile :: String +cacheFile = "data.txt" + +logFile :: String +logFile = "log.txt" + +lightModeScript :: String +lightModeScript = "light.sh" + +darkModeScript :: String +darkModeScript = "dark.sh" diff --git a/app/Getters.hs b/app/Getters.hs new file mode 100644 index 0000000..6239d4d --- /dev/null +++ b/app/Getters.hs @@ -0,0 +1,7 @@ +module Getters where + +import Control.Exception (SomeException, try) +import Network.HTTP.Request (Response, get) + +fetch :: String -> IO (Either SomeException Response) +fetch = try . get diff --git a/app/Main.hs b/app/Main.hs index 94f69de..8b933a8 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,128 +1,35 @@ --- 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) +-- don't add commands to `at` while this script is running (monadic purity must be preserved) module Main where -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 Pure (readLines) +import Cache (dumpCache, start) +import Const (query) +import Sugar (continue, destruct) +import Types (ResponseCode(..), ResponseMsg(..), genericErr, toResponseMsg) +import Getters (fetch) +import Workers (backupRunner, prepareScripts) + 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) +import Network.HTTP.Request (Response(responseBody, responseStatus)) -- error handling --- refactor into modules --- clean up jank (reduce lines in functions, more atomic/functional, etc) +-- clean up jank ( +-- create types, +-- more pure functions, +-- reduce lines in functions, +-- more atomic/functional, +-- etc +-- ) -- eliminate as many do blocks as possible -- introduce liquid types and checking -- whitepaper! -class Status s where - ok :: s -> Bool - ok = const False - - disp :: s -> String - disp = const "encountered bad response (expected OK)" - -newtype ResponseCode = ResponseCode Int -instance Status ResponseCode where - ok (ResponseCode code) = code == 200 - disp (ResponseCode code) = - "encountered bad response code: " - ++ show code - ++ " (expected 200 'OK')" - -newtype ResponseMsg = ResponseMsg String -instance Status ResponseMsg where - ok (ResponseMsg msg) = msg == "success" - disp (ResponseMsg msg) = - "encountered bad response message: " - ++ msg - ++ " (expected 'success')" - -toResponseMsg :: SomeException -> ResponseMsg -toResponseMsg = ResponseMsg . show - -query :: String -query = "http://ip-api.com/line/?fields=status,lat,lon,timezone" - -genericErr :: ResponseMsg -genericErr = ResponseMsg "encountered unknown error" - -prog :: String -prog = "suntheme" - -lightModeScript :: String -lightModeScript = "light.sh" - -darkModeScript :: String -darkModeScript = "dark.sh" - -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 + Nothing -> continue genericErr err Just (msg, lat, lon, tz) -> let status = ResponseMsg msg runner = run lat lon tz @@ -132,114 +39,22 @@ process r run err = 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 +processRunner r = process r run backupRunner + where run lat lon tz = dumpCache lat lon tz >>= prepareScripts -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 "" +ping :: (Response -> IO ()) -> IO () -> IO () +ping run err = do + res <- fetch query + case res of + Left e -> continue (toResponseMsg e) err + Right r -> + let status = (ResponseCode . responseStatus) r + runner = run r + in destruct status runner err routine :: IO () routine = ping processRunner backupRunner main :: IO () -main = start routine +main = start >> routine diff --git a/app/Pure.hs b/app/Pure.hs new file mode 100644 index 0000000..fba5ece --- /dev/null +++ b/app/Pure.hs @@ -0,0 +1,28 @@ +module Pure where + +import Data.Time (ZonedTime) +import Data.Time.RFC3339 (formatTimeRFC3339) +import Data.Time.LocalTime (zonedTimeToUTC) + +readLines :: [String] -> Maybe (String, Double, Double, String) +readLines [msg, latStr, lonStr, tz] = Just (msg, read latStr, read lonStr, tz) +readLines _ = Nothing + +kill :: String -> String +kill = (++) "atrm " + +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 + +sunriseNext :: ZonedTime -> ZonedTime -> ZonedTime -> Bool +sunriseNext sunriseTime sunsetTime time = + let utcTimeNow = zonedTimeToUTC time + utcSunrise = zonedTimeToUTC sunriseTime + utcSunset = zonedTimeToUTC sunsetTime + in utcTimeNow < utcSunrise || utcTimeNow > utcSunset diff --git a/app/Sugar.hs b/app/Sugar.hs new file mode 100644 index 0000000..1d2213a --- /dev/null +++ b/app/Sugar.hs @@ -0,0 +1,31 @@ +module Sugar where + +import Pure (kill) +import Types (Status(..)) + +import System.Exit (ExitCode(ExitFailure), exitWith) +import System.Process (readProcess) +import Control.Exception (SomeException, catch) + +throw :: (Monoid m) => SomeException -> IO m +throw e = print e >> return mempty + +continue :: (Status s) => s -> IO () -> IO () +continue e err = (putStrLn . disp) e >> err + +failure :: SomeException -> IO () +failure = print + +destruct :: (Status s) => s -> IO () -> IO () -> IO () +destruct status success unsuccessful + | ok status = success + | otherwise = continue status unsuccessful + +crash :: IO () +crash = (exitWith . ExitFailure) 1 + +exec :: String -> (SomeException -> IO String) -> IO String +exec cmd = catch (readProcess "bash" ["-c", cmd] "") + +killall :: [String] -> [IO String] +killall = map (dispatch . kill) where dispatch cmd = exec cmd throw diff --git a/app/Time.hs b/app/Time.hs new file mode 100644 index 0000000..93d37c8 --- /dev/null +++ b/app/Time.hs @@ -0,0 +1,44 @@ +module Time where + +import Pure (buildCmd, sunriseNext) +import Cache (pathToConfig) +import Const (darkModeScript, lightModeScript) +import Sugar (exec) + +import Data.Time (ZonedTime, getCurrentTime) +import Data.Time.Solar (Location(Location), sunrise, sunset) +import Data.Time.LocalTime (getTimeZone, utcToZonedTime) + +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 + +activateOnSunrise :: ZonedTime -> ZonedTime -> IO Bool +activateOnSunrise sunriseTime sunsetTime = sunriseNext sunriseTime sunsetTime <$> now + +activate :: String -> ZonedTime -> String -> IO String +activate msg time script = do + putStr msg + print time + scriptPath <- pathToConfig script + exec (buildCmd scriptPath time) terminate + where terminate _ = print "Scheduling process failed" >> return "" + +chooseActivation :: Bool -> ZonedTime -> ZonedTime -> IO String +chooseActivation lightMode sunriseTime sunsetTime + | lightMode = activate "Light mode activation script scheduled for " sunriseTime lightModeScript + | otherwise = activate "Dark mode activation script scheduled for " sunsetTime darkModeScript diff --git a/app/Types.hs b/app/Types.hs new file mode 100644 index 0000000..5601caa --- /dev/null +++ b/app/Types.hs @@ -0,0 +1,32 @@ +module Types where + +import Control.Exception (SomeException) + +class Status s where + ok :: s -> Bool + ok = const False + + disp :: s -> String + disp = const "encountered bad response (expected OK)" + +newtype ResponseCode = ResponseCode Int +instance Status ResponseCode where + ok (ResponseCode code) = code == 200 + disp (ResponseCode code) = + "encountered bad response code: " + ++ show code + ++ " (expected 200 'OK')" + +newtype ResponseMsg = ResponseMsg String +instance Status ResponseMsg where + ok (ResponseMsg msg) = msg == "success" + disp (ResponseMsg msg) = + "encountered bad response message: " + ++ msg + ++ " (expected 'success')" + +toResponseMsg :: SomeException -> ResponseMsg +toResponseMsg = ResponseMsg . show + +genericErr :: ResponseMsg +genericErr = ResponseMsg "encountered unknown error" diff --git a/app/Workers.hs b/app/Workers.hs new file mode 100644 index 0000000..de22849 --- /dev/null +++ b/app/Workers.hs @@ -0,0 +1,26 @@ +module Workers where + +import Time (chooseActivation, activateOnSunrise, sunriseNow, sunsetNow) +import Cache (finish, readCache) +import Sugar (crash, exec) + +prepareScripts :: (Double, Double) -> IO () +prepareScripts (lat, lon) = do + sunriseTime <- sunriseNow lat lon + sunsetTime <- sunsetNow lat lon + lightMode <- activateOnSunrise sunriseTime sunsetTime + _ <- chooseActivation lightMode sunriseTime sunsetTime + queue <- exec "atq" noQueue + finish queue + where + noQueueMsg = "Scheduled process could not be retrieved (try rerunning if 'atq' fails)" + noQueue _ = print noQueueMsg >> return "" + +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) diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..f505f5a --- /dev/null +++ b/flake.lock @@ -0,0 +1,707 @@ +{ + "nodes": { + "HTTP": { + "flake": false, + "locked": { + "lastModified": 1451647621, + "narHash": "sha256-oHIyw3x0iKBexEo49YeUDV1k74ZtyYKGR2gNJXXRxts=", + "owner": "phadej", + "repo": "HTTP", + "rev": "9bc0996d412fef1787449d841277ef663ad9a915", + "type": "github" + }, + "original": { + "owner": "phadej", + "repo": "HTTP", + "type": "github" + } + }, + "cabal-32": { + "flake": false, + "locked": { + "lastModified": 1603716527, + "narHash": "sha256-X0TFfdD4KZpwl0Zr6x+PLxUt/VyKQfX7ylXHdmZIL+w=", + "owner": "haskell", + "repo": "cabal", + "rev": "48bf10787e27364730dd37a42b603cee8d6af7ee", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.2", + "repo": "cabal", + "type": "github" + } + }, + "cabal-34": { + "flake": false, + "locked": { + "lastModified": 1645834128, + "narHash": "sha256-wG3d+dOt14z8+ydz4SL7pwGfe7SiimxcD/LOuPCV6xM=", + "owner": "haskell", + "repo": "cabal", + "rev": "5ff598c67f53f7c4f48e31d722ba37172230c462", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.4", + "repo": "cabal", + "type": "github" + } + }, + "cabal-36": { + "flake": false, + "locked": { + "lastModified": 1669081697, + "narHash": "sha256-I5or+V7LZvMxfbYgZATU4awzkicBwwok4mVoje+sGmU=", + "owner": "haskell", + "repo": "cabal", + "rev": "8fd619e33d34924a94e691c5fea2c42f0fc7f144", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "3.6", + "repo": "cabal", + "type": "github" + } + }, + "cardano-shell": { + "flake": false, + "locked": { + "lastModified": 1608537748, + "narHash": "sha256-PulY1GfiMgKVnBci3ex4ptk2UNYMXqGjJOxcPy2KYT4=", + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "9392c75087cb9a3d453998f4230930dea3a95725", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "cardano-shell", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1672831974, + "narHash": "sha256-z9k3MfslLjWQfnjBtEtJZdq3H7kyi2kQtUThfTgdRk0=", + "owner": "input-output-hk", + "repo": "flake-compat", + "rev": "45f2638735f8cdc40fe302742b79f248d23eb368", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "hkm/gitlab-fix", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "ghc-8.6.5-iohk": { + "flake": false, + "locked": { + "lastModified": 1600920045, + "narHash": "sha256-DO6kxJz248djebZLpSzTGD6s8WRpNI9BTwUeOf5RwY8=", + "owner": "input-output-hk", + "repo": "ghc", + "rev": "95713a6ecce4551240da7c96b6176f980af75cae", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "ref": "release/8.6.5-iohk", + "repo": "ghc", + "type": "github" + } + }, + "hackage": { + "flake": false, + "locked": { + "lastModified": 1723768183, + "narHash": "sha256-YG61ZL5YZvSWPvdusFsS8EYDJ/MW+EgX8fjNMWU18GQ=", + "owner": "input-output-hk", + "repo": "hackage.nix", + "rev": "37977e3b6201db1e0f3c62d9c55ec20dfcee10a2", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "hackage.nix", + "type": "github" + } + }, + "haskellNix": { + "inputs": { + "HTTP": "HTTP", + "cabal-32": "cabal-32", + "cabal-34": "cabal-34", + "cabal-36": "cabal-36", + "cardano-shell": "cardano-shell", + "flake-compat": "flake-compat", + "ghc-8.6.5-iohk": "ghc-8.6.5-iohk", + "hackage": "hackage", + "hls-1.10": "hls-1.10", + "hls-2.0": "hls-2.0", + "hls-2.2": "hls-2.2", + "hls-2.3": "hls-2.3", + "hls-2.4": "hls-2.4", + "hls-2.5": "hls-2.5", + "hls-2.6": "hls-2.6", + "hls-2.7": "hls-2.7", + "hls-2.8": "hls-2.8", + "hls-2.9": "hls-2.9", + "hpc-coveralls": "hpc-coveralls", + "hydra": "hydra", + "iserv-proxy": "iserv-proxy", + "nixpkgs": [ + "haskellNix", + "nixpkgs-unstable" + ], + "nixpkgs-2003": "nixpkgs-2003", + "nixpkgs-2105": "nixpkgs-2105", + "nixpkgs-2111": "nixpkgs-2111", + "nixpkgs-2205": "nixpkgs-2205", + "nixpkgs-2211": "nixpkgs-2211", + "nixpkgs-2305": "nixpkgs-2305", + "nixpkgs-2311": "nixpkgs-2311", + "nixpkgs-2405": "nixpkgs-2405", + "nixpkgs-unstable": "nixpkgs-unstable", + "old-ghc-nix": "old-ghc-nix", + "stackage": "stackage" + }, + "locked": { + "lastModified": 1723769442, + "narHash": "sha256-dGhNJril+hILFro59PdYWwKZYJ6/2A1hozTanaAu4lE=", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "517ceaa97e38ae211d9c311952cc8be4a4a026cd", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "haskell.nix", + "type": "github" + } + }, + "hls-1.10": { + "flake": false, + "locked": { + "lastModified": 1680000865, + "narHash": "sha256-rc7iiUAcrHxwRM/s0ErEsSPxOR3u8t7DvFeWlMycWgo=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "b08691db779f7a35ff322b71e72a12f6e3376fd9", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "1.10.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.0": { + "flake": false, + "locked": { + "lastModified": 1687698105, + "narHash": "sha256-OHXlgRzs/kuJH8q7Sxh507H+0Rb8b7VOiPAjcY9sM1k=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "783905f211ac63edf982dd1889c671653327e441", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.0.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.2": { + "flake": false, + "locked": { + "lastModified": 1693064058, + "narHash": "sha256-8DGIyz5GjuCFmohY6Fa79hHA/p1iIqubfJUTGQElbNk=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "b30f4b6cf5822f3112c35d14a0cba51f3fe23b85", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.2.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.3": { + "flake": false, + "locked": { + "lastModified": 1695910642, + "narHash": "sha256-tR58doOs3DncFehHwCLczJgntyG/zlsSd7DgDgMPOkI=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "458ccdb55c9ea22cd5d13ec3051aaefb295321be", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.3.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.4": { + "flake": false, + "locked": { + "lastModified": 1699862708, + "narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.4.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.5": { + "flake": false, + "locked": { + "lastModified": 1701080174, + "narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "27f8c3d3892e38edaef5bea3870161815c4d014c", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.5.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.6": { + "flake": false, + "locked": { + "lastModified": 1705325287, + "narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.6.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.7": { + "flake": false, + "locked": { + "lastModified": 1708965829, + "narHash": "sha256-LfJ+TBcBFq/XKoiNI7pc4VoHg4WmuzsFxYJ3Fu+Jf+M=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "50322b0a4aefb27adc5ec42f5055aaa8f8e38001", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.7.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.8": { + "flake": false, + "locked": { + "lastModified": 1715153580, + "narHash": "sha256-Vi/iUt2pWyUJlo9VrYgTcbRviWE0cFO6rmGi9rmALw0=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "dd1be1beb16700de59e0d6801957290bcf956a0a", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.8.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.9": { + "flake": false, + "locked": { + "lastModified": 1718469202, + "narHash": "sha256-THXSz+iwB1yQQsr/PY151+2GvtoJnTIB2pIQ4OzfjD4=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "40891bccb235ebacce020b598b083eab9dda80f1", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.9.0.0", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hpc-coveralls": { + "flake": false, + "locked": { + "lastModified": 1607498076, + "narHash": "sha256-8uqsEtivphgZWYeUo5RDUhp6bO9j2vaaProQxHBltQk=", + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "rev": "14df0f7d229f4cd2e79f8eabb1a740097fdfa430", + "type": "github" + }, + "original": { + "owner": "sevanspowell", + "repo": "hpc-coveralls", + "type": "github" + } + }, + "hydra": { + "inputs": { + "nix": "nix", + "nixpkgs": [ + "haskellNix", + "hydra", + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1671755331, + "narHash": "sha256-hXsgJj0Cy0ZiCiYdW2OdBz5WmFyOMKuw4zyxKpgUKm4=", + "owner": "NixOS", + "repo": "hydra", + "rev": "f48f00ee6d5727ae3e488cbf9ce157460853fea8", + "type": "github" + }, + "original": { + "id": "hydra", + "type": "indirect" + } + }, + "iserv-proxy": { + "flake": false, + "locked": { + "lastModified": 1717479972, + "narHash": "sha256-7vE3RQycHI1YT9LHJ1/fUaeln2vIpYm6Mmn8FTpYeVo=", + "owner": "stable-haskell", + "repo": "iserv-proxy", + "rev": "2ed34002247213fc435d0062350b91bab920626e", + "type": "github" + }, + "original": { + "owner": "stable-haskell", + "ref": "iserv-syms", + "repo": "iserv-proxy", + "type": "github" + } + }, + "lowdown-src": { + "flake": false, + "locked": { + "lastModified": 1633514407, + "narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=", + "owner": "kristapsdz", + "repo": "lowdown", + "rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8", + "type": "github" + }, + "original": { + "owner": "kristapsdz", + "repo": "lowdown", + "type": "github" + } + }, + "nix": { + "inputs": { + "lowdown-src": "lowdown-src", + "nixpkgs": "nixpkgs", + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1661606874, + "narHash": "sha256-9+rpYzI+SmxJn+EbYxjGv68Ucp22bdFUSy/4LkHkkDQ=", + "owner": "NixOS", + "repo": "nix", + "rev": "11e45768b34fdafdcf019ddbd337afa16127ff0f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "2.11.0", + "repo": "nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1657693803, + "narHash": "sha256-G++2CJ9u0E7NNTAi9n5G8TdDmGJXcIjkJ3NF8cetQB8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "365e1b3a859281cf11b94f87231adeabbdd878a2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-22.05-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2003": { + "locked": { + "lastModified": 1620055814, + "narHash": "sha256-8LEHoYSJiL901bTMVatq+rf8y7QtWuZhwwpKE2fyaRY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1db42b7fe3878f3f5f7a4f2dc210772fd080e205", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-20.03-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2105": { + "locked": { + "lastModified": 1659914493, + "narHash": "sha256-lkA5X3VNMKirvA+SUzvEhfA7XquWLci+CGi505YFAIs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "022caabb5f2265ad4006c1fa5b1ebe69fb0c3faf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2111": { + "locked": { + "lastModified": 1659446231, + "narHash": "sha256-hekabNdTdgR/iLsgce5TGWmfIDZ86qjPhxDg/8TlzhE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "eabc38219184cc3e04a974fe31857d8e0eac098d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-21.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2205": { + "locked": { + "lastModified": 1685573264, + "narHash": "sha256-Zffu01pONhs/pqH07cjlF10NnMDLok8ix5Uk4rhOnZQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "380be19fbd2d9079f677978361792cb25e8a3635", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-22.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2211": { + "locked": { + "lastModified": 1688392541, + "narHash": "sha256-lHrKvEkCPTUO+7tPfjIcb7Trk6k31rz18vkyqmkeJfY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ea4c80b39be4c09702b0cb3b42eab59e2ba4f24b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-22.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2305": { + "locked": { + "lastModified": 1705033721, + "narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2311": { + "locked": { + "lastModified": 1719957072, + "narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7144d6241f02d171d25fba3edeaf15e0f2592105", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-23.11-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-2405": { + "locked": { + "lastModified": 1720122915, + "narHash": "sha256-Nby8WWxj0elBu1xuRaUcRjPi/rU3xVbkAt2kj4QwX2U=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "835cf2d3f37989c5db6585a28de967a667a75fb1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-24.05-darwin", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1720181791, + "narHash": "sha256-i4vJL12/AdyuQuviMMd1Hk2tsGt02hDNhA0Zj1m16N8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "4284c2b73c8bce4b46a6adf23e16d9e2ec8da4bb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "old-ghc-nix": { + "flake": false, + "locked": { + "lastModified": 1631092763, + "narHash": "sha256-sIKgO+z7tj4lw3u6oBZxqIhDrzSkvpHtv0Kki+lh9Fg=", + "owner": "angerman", + "repo": "old-ghc-nix", + "rev": "af48a7a7353e418119b6dfe3cd1463a657f342b8", + "type": "github" + }, + "original": { + "owner": "angerman", + "ref": "master", + "repo": "old-ghc-nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "haskellNix": "haskellNix", + "nixpkgs": [ + "haskellNix", + "nixpkgs-unstable" + ] + } + }, + "stackage": { + "flake": false, + "locked": { + "lastModified": 1723594352, + "narHash": "sha256-cQVhF1M1et3/XNE1sclwH39prxIDMUojTdnW61t3YrM=", + "owner": "input-output-hk", + "repo": "stackage.nix", + "rev": "c077da02c56031a78adc4bf0cf2b182effc895ed", + "type": "github" + }, + "original": { + "owner": "input-output-hk", + "repo": "stackage.nix", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c985748 --- /dev/null +++ b/flake.nix @@ -0,0 +1,56 @@ +{ + description = "Suntheme's development and build environment"; + + inputs.haskellNix.url = "github:input-output-hk/haskell.nix"; + inputs.nixpkgs.follows = "haskellNix/nixpkgs-unstable"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + outputs = { + self, + nixpkgs, + flake-utils, + haskellNix, + }: let + supportedSystems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + ]; + in + flake-utils.lib.eachSystem supportedSystems (system: let + overlays = [ + haskellNix.overlay + (final: prev: { + suntheme = final.haskell-nix.project' { + src = ./.; + compiler-nix-name = "ghc982"; + shell.tools = { + cabal = {}; + hlint = {}; + haskell-language-server = {}; + }; + shell.buildInputs = with pkgs; [ + at + ]; + }; + }) + ]; + pkgs = import nixpkgs { + inherit system overlays; + inherit (haskellNix) config; + }; + flake = pkgs.suntheme.flake {}; + in + flake + // { + formatter = pkgs.alejandra; + + packages.default = flake.packages."suntheme:exe:suntheme"; + }); + + nixConfig = { + extra-substituters = ["https://cache.iog.io" "https://suntheme.cachix.org"]; + extra-trusted-public-keys = ["hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ=" "suntheme.cachix.org-1:fHjlz7YAmMUcLp3tsZis8g9wIsDS6HvECGR3uZETGRo="]; + allow-import-from-derivation = "true"; + }; +} diff --git a/suntheme.cabal b/suntheme.cabal index fef2dfb..206cc85 100644 --- a/suntheme.cabal +++ b/suntheme.cabal @@ -62,22 +62,22 @@ executable suntheme main-is: Main.hs -- Modules included in this executable, other than Main. - -- other-modules: + other-modules: Types, Const, Pure, Time, Sugar, Getters, Cache, Workers -- LANGUAGE extensions used by modules in this package. -- other-extensions: -- Other library packages from which modules are imported. - build-depends: base ^>=4.17.2.1, - request ^>=0.2.2.0, - process ^>=1.6.18.0, - bytestring ^>=0.11.5.3, - filepath ^>=1.4.2.2, - time ^>=1.9.3, - solar ^>=0.1.0.0, - timerep ^>=2.1.0.0, - extra ^>=1.7.16, - directory ^>=1.3.8.5 + build-depends: base ^>=4.19.1.0, + request ^>=0.2.2.0, + process ^>=1.6.18.0, + bytestring ^>=0.11.5.3, + filepath ^>=1.4.2.2, + time ^>=1.9.3, + solar ^>=0.1.0.0, + timerep ^>=2.1.0.0, + extra ^>=1.7.16, + directory ^>=1.3.8.5 -- Directories containing source files. hs-source-dirs: app