packages feed

diohsc-0.1.14: diohsc.hs

-- This file is part of Diohsc
-- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

{-# LANGUAGE CPP               #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Control.Monad.Catch
import           Control.Monad.State

import           Data.Hash                (hash)
import           Data.Maybe
import qualified Data.Set                 as S
import qualified Data.Text.Lazy           as T
import qualified Data.Text.Lazy.IO        as T

import qualified System.Console.Haskeline as HL
import           System.Directory
import           System.Environment
import           System.Exit
import           System.FilePath
import           System.IO


import qualified BStack
import           ClientOptions
import           ClientState
import           GeminiProtocol
import           LineClient
import           Marks
import           Mundanities
import           Opts
import           PrintFancy
import           Prompt                   hiding (promptYN)
import qualified Prompt
import           URI
import           Version

#ifndef WINDOWS
import           System.Posix.Files       (ownerModes, setFileMode)
#endif

main :: IO ()
main = do
    argv <- getArgs
    (opts,args) <- parseArgs argv
    when (Help `elem` opts) $ putStr usage >> exitSuccess
    when (Version `elem` opts) $ putStrLn version >> exitSuccess

    defUserDataDir <- getAppUserDataDirectory programName
    userDataDir <- canonicalizePath . fromMaybe defUserDataDir $ listToMaybe [ path | DataDir path <- opts ]
    let restrictedMode = Restricted `elem` opts

    outTerm <- hIsTerminalDevice stdout
    let ansi = NoAnsi `notElem` opts && (outTerm || Ansi `elem` opts)

    let argCommands (ScriptFile "-") = warnIOErrAlt $
            (T.unpack . T.strip <$>) . T.lines <$> T.getContents
        argCommands (ScriptFile f) = warnIOErrAlt $ (T.unpack <$>) <$> readFileLines f
        argCommands (OptCommand c) = return [c]
        argCommands _ = return []
    optCommands <- concat <$> mapM argCommands opts
    let repl = (null optCommands && Batch `notElem` opts) || Prompt `elem` opts
    let interactive = Batch `notElem` opts && (repl || Interactive `elem` opts)

    let argToUri arg = doesPathExist arg >>= \case
            True -> Just . ("file://" <>) . escapePathString <$> makeAbsolute arg
            False | Just uri <- parseUriAsAbsolute . escapeIRI $ arg -> return $ Just $ show uri
            _ -> printErrFancy ansi ("No such URI / file: " <> arg) >> return Nothing
    argCommand <- join <$> mapM argToUri (listToMaybe args)

    let initialCommands = optCommands ++ maybeToList argCommand

    let ghost = Ghost `elem` opts

    unless ghost $ do
        mkdirhier userDataDir
#ifndef WINDOWS
        setFileMode userDataDir ownerModes -- chmod 700
#endif

    let cmdHistoryPath = userDataDir </> "commandHistory"
        marksPath = userDataDir </> "marks"
        logPath = userDataDir </> "log"

    let displayInfo :: [String] -> IO ()
        displayInfo = mapM_ $ printInfoFancy ansi
        displayWarning = mapM_ $ printErrFancy ansi
        promptYN = Prompt.promptYN interactive
        callbacks = InteractionCallbacks displayInfo displayWarning waitKey promptYN
        socksProxy = maybe (const NoSocksProxy) Socks5Proxy
            (listToMaybe [ h | SocksHost h <- opts ])
            . fromMaybe "1080" $ listToMaybe [ p | SocksPort p <- opts ]

    requestContext <- initRequestContext callbacks userDataDir ghost socksProxy
    (warnings, marks) <- loadMarks marksPath
    displayWarning warnings
    let hlSettings = (HL.defaultSettings::HL.Settings ClientM)
            { HL.complete = HL.noCompletion
            , HL.historyFile = if ghost then Nothing else Just cmdHistoryPath
            }

    cLog <- BStack.fromList . reverse <$> readFileLines logPath
    let visited = S.fromList $ hash . T.unpack <$> BStack.toList cLog

    let openLog :: IO (Maybe Handle)
        openLog = ignoreIOErrAlt $ Just <$> do
            h <- openFile logPath AppendMode
            hSetBuffering h LineBuffering
            return h
        closeLog :: Maybe Handle -> IO ()
        closeLog = maybe (return ()) hClose

    (if ghost then ($ Nothing) else bracketOnError openLog closeLog) $ \logH ->
        let clientOptions = ClientOptions userDataDir interactive ansi ghost
                restrictedMode requestContext logH
            initState = emptyClientState {clientMarks = marks
                , clientLog = cLog, clientVisited = visited}
        in do
            endState <- (`execStateT` initState) . HL.runInputT hlSettings $
                lineClient clientOptions initialCommands repl
            closeLog logH
            -- |reread file rather than just writing clientLog, in case another instance has also
            -- been appending to the log.
            unless ghost . warnIOErr $ truncateToEnd (clientConfMaxLogLen $ clientConfig endState) logPath