More work

This commit is contained in:
Julian Ospald 2022-12-06 20:21:38 +08:00
parent 6a5043b68f
commit efb81e4bac
Signed by: hasufell
GPG Key ID: 3786C5262ECB4A3F

View File

@ -14,10 +14,15 @@ import GHCup.Prelude ( decUTF8Safe )
import GHCup.Prelude.File import GHCup.Prelude.File
import GHCup.Prelude.Logger import GHCup.Prelude.Logger
import GHCup.Prelude.Process import GHCup.Prelude.Process
import GHCup.Prompts
import GHCup.Types hiding ( LeanAppState(..) ) import GHCup.Types hiding ( LeanAppState(..) )
import GHCup.Types.Optics ( getDirs ) import GHCup.Types.Optics ( getDirs )
import GHCup.Utils import GHCup.Utils
import Data.List (sort, intersperse)
import Data.Versions (prettyPVP)
import Data.Maybe (catMaybes)
import Codec.Archive
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
-- import System.Console.ANSI -- import System.Console.ANSI
@ -27,9 +32,13 @@ import System.Console.ANSI.Types
import Terminal.Game import Terminal.Game
import Text.PrettyPrint.HughesPJClass ( prettyShow ) import Text.PrettyPrint.HughesPJClass ( prettyShow )
import Control.Monad ( join, when ) import Control.Exception.Safe
import Control.Monad ( join, when, void, forM_ )
import Control.Monad.ST import Control.Monad.ST
import Control.Monad.Reader ( ReaderT(runReaderT) ) import Control.Monad.Reader ( ReaderT(runReaderT), MonadReader, ask, lift )
import Control.Monad.Trans.Except
import Control.Monad.Trans.Resource
import Data.Functor
import Data.Bifunctor import Data.Bifunctor
import Data.STRef import Data.STRef
import Data.IORef import Data.IORef
@ -40,7 +49,15 @@ import qualified Data.Vector as V
import GHC.IO ( unsafePerformIO ) import GHC.IO ( unsafePerformIO )
import Haskus.Utils.Variant.Excepts import Haskus.Utils.Variant.Excepts
import System.Exit import System.Exit
import System.Environment (getExecutablePath)
import Data.Versions (prettyVer) import Data.Versions (prettyVer)
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as B
import qualified Data.Text.Lazy as L
import System.FilePath
import URI.ByteString (serializeURIRef')
data Direction = Up data Direction = Up
| Down | Down
@ -69,10 +86,22 @@ data BrickState = BrickState
, appState :: BrickInternalState , appState :: BrickInternalState
, appKeys :: KeyBindings , appKeys :: KeyBindings
, appQuit :: Bool , appQuit :: Bool
, appRestart :: Bool
, appMoreInput :: Maybe String , appMoreInput :: Maybe String
} }
deriving Show deriving Show
startGame :: BrickState -> IO BrickState
startGame g = do
g'@BrickState { appRestart } <- errorPress $ playGameT liftIO (ghcupGame g)
if appRestart
then do
putStrLn "Press enter to continue"
_ <- getLine
startGame $ g' { appRestart = False }
else pure g'
ansiMain :: AppState -> IO () ansiMain :: AppState -> IO ()
ansiMain s = do ansiMain s = do
writeIORef settings' s writeIORef settings' s
@ -85,10 +114,12 @@ ansiMain s = do
(constructList ad defaultAppSettings Nothing) (constructList ad defaultAppSettings Nothing)
(keyBindings (s :: AppState)) (keyBindings (s :: AppState))
False False
False
Nothing Nothing
errorPress $ playGame (ghcupGame g) void $ startGame g
cleanAndExit
Left e -> do Left e -> do
flip runReaderT s $ logError $ "Error building app state: " <> Tx.pack flip runReaderT s $ logError $ "Error building app state: " <> Tx.pack
(show e) (show e)
@ -105,37 +136,61 @@ ghcupGame bs = Game 13
bs -- ticks per second bs -- ticks per second
(\ge s e -> logicFun ge s e) -- logic function (\ge s e -> logicFun ge s e) -- logic function
(\r s -> centerFull r $ drawFun s r) -- draw function (\r s -> centerFull r $ drawFun s r) -- draw function
appQuit -- quit function (\bs -> appQuit bs || appRestart bs) -- quit function
drawFun :: BrickState -> GEnv -> Plane drawFun :: BrickState -> GEnv -> Plane
drawFun (BrickState {..}) GEnv{..} = drawFun (BrickState {..}) GEnv{..} =
blankPlane mw mh blankPlane mw mh
& (1, 1) % box 1 1 '┌' & (1, 1) % box 1 1 '┌'
& (2, 1) % box 1 (mh - 2) '│' & (2, 1) % box 1 (mh - 3) '│'
& (1, 2) % box (mw - 2) 1 '─' & (1, 2) % box (mw - 2) 1 '─'
& (2, mw) % box 1 (mh - 2) '│' & (2, mw) % box 1 (mh - 3) '│'
& (1, mw) % box 1 1 '┐' & (1, mw) % box 1 1 '┐'
& (mh, 2) % box (mw - 2) 1 '─' & (mh-1, 2) % box (mw - 2) 1 '─'
& (mh, 1) % box 1 1 '└' & (mh-1, 1) % box 1 1 '└'
& (mh, mw) % box 1 1 '┘' & (mh-1, mw) % box 1 1 '┘'
& (2, 2) % box (mw - 2) (mh - 2) ' ' & (2, 2) % box (mw - 2) (mh - 3) ' '
& (2, 2) % renderItems & (2, 2) % (header === box (mw - 2) 1 '─' === renderItems)
& (mh, 1) % footer
& (1, mw `div` 2 - 2) % stringPlane "GHCup"
where where
mh :: Height mh :: Height
mw :: Width mw :: Width
(mh, mw) = T.swap eTermDims (mh, mw) = T.swap eTermDims
footer = hcat
. intersperse (stringPlane " ")
. fmap stringPlane
$ ["q:Quit", "i:Install", "u:Uninstall", "s:Set", "c:Changelog", "a:all versions", "↑:Up", "↓:Down"]
header = hcat
. intersperse space
. fmap stringPlane
$ ["Tool", "Version", "Tags", "Notes"]
renderItems = drawListElements renderItem True appState renderItems = drawListElements renderItem True appState
renderItem _ b ListResult{..} = renderItem _ b listResult@ListResult{..} =
let marks = if let marks = if
| lSet -> (stringPlane "✔✔") | lSet -> color Green Vivid (stringPlane "✔✔")
| lInstalled -> (stringPlane "") | lInstalled -> color Green Dull (stringPlane "")
| otherwise -> (stringPlane "") | otherwise -> color Red Vivid (stringPlane "")
ver = case lCross of ver = case lCross of
Nothing -> stringPlane . Tx.unpack . prettyVer $ lVer Nothing -> stringPlane . Tx.unpack . prettyVer $ lVer
Just c -> stringPlane . Tx.unpack $ (c <> "-" <> prettyVer lVer) Just c -> stringPlane . Tx.unpack $ (c <> "-" <> prettyVer lVer)
tool = printTool lTool tool = printTool lTool
in marks ||| space ||| space ||| tool ||| space ||| ver tag = let l = catMaybes . fmap printTag $ sort lTag
in if null l then blankPlane 1 1 else foldr1 (\x y -> x ||| stringPlane "," ||| y) l
notes = let n = printNotes listResult
in if null n
then blankPlane 1 1
else foldr1 (\x y -> x ||| stringPlane "," ||| y) n
in hcat [marks, space, space, tool, space, ver, space, tag, space, notes]
printTag Recommended = Just $ color Green Dull $ stringPlane "recommended"
printTag Latest = Just $ color Yellow Dull $ stringPlane "latest"
printTag Prerelease = Just $ color Red Dull $ stringPlane "prerelease"
printTag (Base pvp'') = Just $ stringPlane ("base-" ++ T.unpack (prettyPVP pvp''))
printTag Old = Nothing
printTag (UnknownTag t) = Just $ stringPlane t
printTool Cabal = stringPlane "cabal" printTool Cabal = stringPlane "cabal"
printTool GHC = stringPlane "GHC" printTool GHC = stringPlane "GHC"
@ -143,6 +198,12 @@ drawFun (BrickState {..}) GEnv{..} =
printTool HLS = stringPlane "HLS" printTool HLS = stringPlane "HLS"
printTool Stack = stringPlane "Stack" printTool Stack = stringPlane "Stack"
printNotes ListResult {..} =
(if hlsPowered then [color Green Dull $ stringPlane "hls-powered"] else mempty
)
++ (if fromSrc then [color Blue Dull $ stringPlane "compiled"] else mempty)
++ (if lStray then [color Blue Dull $ stringPlane "stray"] else mempty)
space = stringPlane " " space = stringPlane " "
-- | Draws the list elements. -- | Draws the list elements.
@ -183,7 +244,7 @@ drawFun (BrickState {..}) GEnv{..} =
pure $ V.fromList [markSelected elemWidget] pure $ V.fromList [markSelected elemWidget]
i <- readSTRef ref i <- readSTRef ref
pure (arr, i) pure (arr, i)
in vcat $ V.toList (makeVisible drawnElements (mh - 2) selIx) in vcat $ V.toList (makeVisible drawnElements (mh - 5) selIx)
where where
makeVisible :: V.Vector Plane -> Height -> Maybe Int -> V.Vector Plane makeVisible :: V.Vector Plane -> Height -> Maybe Int -> V.Vector Plane
makeVisible listElements drawableHeight (Just ix) = makeVisible listElements drawableHeight (Just ix) =
@ -202,17 +263,55 @@ drawFun (BrickState {..}) GEnv{..} =
hBorder = box (mw - 2) 1 '─' hBorder = box (mw - 2) 1 '─'
logicFun :: GEnv -> BrickState -> Event -> BrickState logicFun :: GEnv -> BrickState -> Event -> IO BrickState
logicFun _ gs (KeyPress 'q') = gs { appQuit = True } logicFun _ gs (KeyPress 'q') = pure gs { appQuit = True }
logicFun _ gs Tick = gs logicFun _ gs Tick = pure gs
logicFun _ gs@BrickState{appMoreInput = Nothing} (KeyPress '\ESC') = gs { appMoreInput = Just "\ESC" } logicFun _ gs@BrickState{appMoreInput = Nothing} (KeyPress '\ESC') = pure gs { appMoreInput = Just "\ESC" }
logicFun _ gs@BrickState{appMoreInput = Just "\ESC"} (KeyPress '[') = gs { appMoreInput = Just "\ESC[" } logicFun _ gs@BrickState{appMoreInput = Just "\ESC"} (KeyPress '[') = pure gs { appMoreInput = Just "\ESC[" }
logicFun _ gs@BrickState{appMoreInput = Just "\ESC[", appState = s'} (KeyPress 'A') logicFun _ gs@BrickState{appMoreInput = Just "\ESC[", appState = s'} (KeyPress 'A')
= gs { appMoreInput = Nothing, appState = moveCursor 1 s' Up } = pure gs { appMoreInput = Nothing, appState = moveCursor 1 s' Up }
logicFun _ gs@BrickState{appMoreInput = Just "\ESC[", appState = s'} (KeyPress 'B') logicFun _ gs@BrickState{appMoreInput = Just "\ESC[", appState = s'} (KeyPress 'B')
= gs { appMoreInput = Nothing, appState = moveCursor 1 s' Down } = pure gs { appMoreInput = Nothing, appState = moveCursor 1 s' Down }
logicFun _ gs@BrickState{appMoreInput = Just _} _ = gs { appMoreInput = Nothing } logicFun _ gs@BrickState{appMoreInput = Just _} _ = pure gs { appMoreInput = Nothing }
logicFun _ gs (KeyPress c) = gs logicFun _ gs (KeyPress 'i') = do
bs <- withIOAction install' gs
pure bs { appRestart = True }
logicFun _ gs (KeyPress 'u') = do
bs <- withIOAction del' gs
pure bs { appRestart = True }
logicFun _ gs (KeyPress 's') = do
bs <- withIOAction set' gs
pure bs { appRestart = True }
logicFun _ gs (KeyPress 'c') = do
bs <- withIOAction changelog' gs
pure bs { appRestart = True }
logicFun _ gs (KeyPress 'a') = pure $ hideShowHandler (not . showAllVersions) showAllTools gs
where
hideShowHandler :: (BrickSettings -> Bool) -> (BrickSettings -> Bool) -> BrickState -> BrickState
hideShowHandler f p BrickState{..} =
let newAppSettings = appSettings { showAllVersions = f appSettings , showAllTools = p appSettings }
newInternalState = constructList appData newAppSettings (Just appState)
in BrickState appData newAppSettings newInternalState appKeys appQuit appRestart appMoreInput
logicFun _ gs (KeyPress c) = pure gs
withIOAction :: (BrickState
-> (Int, ListResult)
-> ReaderT AppState IO (Either String a))
-> BrickState
-> IO BrickState
withIOAction action as = case listSelectedElement' (appState as) of
Nothing -> pure as
Just (ix, e) -> do
clearScreen
settings <- readIORef settings'
flip runReaderT settings $ action as (ix, e) >>= \case
Left err -> liftIO $ putStrLn ("Error: " <> err)
Right _ -> liftIO $ putStrLn "Success"
getAppData Nothing >>= \case
Right data' -> do
pure (updateList data' as)
Left err -> throwIO $ userError err
moveCursor :: Int -> BrickInternalState -> Direction -> BrickInternalState moveCursor :: Int -> BrickInternalState -> Direction -> BrickInternalState
moveCursor steps ais@BrickInternalState{..} direction = moveCursor steps ais@BrickInternalState{..} direction =
@ -225,6 +324,20 @@ defaultAppSettings :: BrickSettings
defaultAppSettings = defaultAppSettings =
BrickSettings { showAllVersions = False, showAllTools = False } BrickSettings { showAllVersions = False, showAllTools = False }
-- | Update app data and list internal state based on new evidence.
-- This synchronises @BrickInternalState@ with @BrickData@
-- and @BrickSettings@.
updateList :: BrickData -> BrickState -> BrickState
updateList appD BrickState{..} =
let newInternalState = constructList appD appSettings (Just appState)
in BrickState { appState = newInternalState
, appData = appD
, appSettings = appSettings
, appKeys = appKeys
, appQuit = appQuit
, appRestart = appRestart
, appMoreInput = appMoreInput
}
constructList constructList
:: BrickData :: BrickData
@ -282,6 +395,172 @@ listSelectedElement' BrickInternalState {..} = fmap (ix, ) $ clr V.!? ix
boundaries :: (Coords, Coords) boundaries :: (Coords, Coords)
boundaries = ((1, 1), (24, 80)) boundaries = ((1, 1), (24, 80))
install' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m)
=> BrickState
-> (Int, ListResult)
-> m (Either String ())
install' _ (_, ListResult {..}) = do
AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
let run =
runResourceT
. runE
@'[ AlreadyInstalled
, ArchiveResult
, UnknownArchive
, FileDoesNotExistError
, CopyError
, NoDownload
, NotInstalled
, BuildFailed
, TagNotFound
, DigestError
, GPGError
, DownloadFailed
, DirNotEmpty
, NoUpdate
, TarDirDoesNotExist
, FileAlreadyExistsError
, ProcessError
, ToolShadowed
, UninstallFailed
, MergeFileTreeError
]
run (do
ce <- liftIO $ fmap (either (const Nothing) Just) $
try @_ @SomeException $ getExecutablePath >>= canonicalizePath
dirs <- lift getDirs
case lTool of
GHC -> do
let vi = getVersionInfo lVer GHC dls
liftE $ installGHCBin lVer GHCupInternal False [] $> (vi, dirs, ce)
Cabal -> do
let vi = getVersionInfo lVer Cabal dls
liftE $ installCabalBin lVer GHCupInternal False $> (vi, dirs, ce)
GHCup -> do
let vi = snd <$> getLatest dls GHCup
liftE $ upgradeGHCup Nothing False False $> (vi, dirs, ce)
HLS -> do
let vi = getVersionInfo lVer HLS dls
liftE $ installHLSBin lVer GHCupInternal False $> (vi, dirs, ce)
Stack -> do
let vi = getVersionInfo lVer Stack dls
liftE $ installStackBin lVer GHCupInternal False $> (vi, dirs, ce)
)
>>= \case
VRight (vi, Dirs{..}, Just ce) -> do
forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg
case lTool of
GHCup -> do
up <- liftIO $ fmap (either (const Nothing) Just)
$ try @_ @SomeException $ canonicalizePath (binDir </> "ghcup" <.> exeExt)
when ((normalise <$> up) == Just (normalise ce)) $
-- TODO: track cli arguments of previous invocation
void $ liftIO $ exec ce ["tui"] Nothing Nothing
logInfo "Please restart 'ghcup' for the changes to take effect"
_ -> pure ()
pure $ Right ()
VRight (vi, _, _) -> do
forM_ (_viPostInstall =<< vi) $ \msg -> logInfo msg
logInfo "Please restart 'ghcup' for the changes to take effect"
pure $ Right ()
VLeft (V (AlreadyInstalled _ _)) -> pure $ Right ()
VLeft (V NoUpdate) -> pure $ Right ()
VLeft e -> pure $ Left $ prettyShow e <> "\n"
<> "Also check the logs in ~/.ghcup/logs"
set' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m)
=> BrickState
-> (Int, ListResult)
-> m (Either String ())
set' bs input@(_, ListResult {..}) = do
settings <- liftIO $ readIORef settings'
let run =
flip runReaderT settings
. runE @'[FileDoesNotExistError , NotInstalled , TagNotFound]
run (do
case lTool of
GHC -> liftE $ setGHC (GHCTargetVersion lCross lVer) SetGHCOnly Nothing $> ()
Cabal -> liftE $ setCabal lVer $> ()
HLS -> liftE $ setHLS lVer SetHLSOnly Nothing $> ()
Stack -> liftE $ setStack lVer $> ()
GHCup -> pure ()
)
>>= \case
VRight _ -> pure $ Right ()
VLeft e -> case e of
(V (NotInstalled tool _)) -> do
promptAnswer <- getUserPromptResponse userPrompt
case promptAnswer of
PromptYes -> do
res <- install' bs input
case res of
(Left err) -> pure $ Left err
(Right _) -> do
logInfo "Setting now..."
set' bs input
PromptNo -> pure $ Left (prettyShow e)
where
userPrompt = L.toStrict . B.toLazyText . B.fromString $
"This Version of "
<> show tool
<> " you are trying to set is not installed.\n"
<> "Would you like to install it first? [Y/N]: "
_ -> pure $ Left (prettyShow e)
del' :: (MonadReader AppState m, MonadIO m, MonadFail m, MonadMask m, MonadUnliftIO m)
=> BrickState
-> (Int, ListResult)
-> m (Either String ())
del' _ (_, ListResult {..}) = do
AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
let run = runE @'[NotInstalled, UninstallFailed]
run (do
let vi = getVersionInfo lVer lTool dls
case lTool of
GHC -> liftE $ rmGHCVer (GHCTargetVersion lCross lVer) $> vi
Cabal -> liftE $ rmCabalVer lVer $> vi
HLS -> liftE $ rmHLSVer lVer $> vi
Stack -> liftE $ rmStackVer lVer $> vi
GHCup -> pure Nothing
)
>>= \case
VRight vi -> do
forM_ (_viPostRemove =<< vi) $ \msg ->
logInfo msg
pure $ Right ()
VLeft e -> pure $ Left (prettyShow e)
changelog' :: (MonadReader AppState m, MonadIO m)
=> BrickState
-> (Int, ListResult)
-> m (Either String ())
changelog' _ (_, ListResult {..}) = do
AppState { pfreq, ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
case getChangeLog dls lTool (Left lVer) of
Nothing -> pure $ Left $
"Could not find ChangeLog for " <> prettyShow lTool <> ", version " <> T.unpack (prettyVer lVer)
Just uri -> do
let cmd = case _rPlatform pfreq of
Darwin -> "open"
Linux _ -> "xdg-open"
FreeBSD -> "xdg-open"
Windows -> "start"
exec cmd [T.unpack $ decUTF8Safe $ serializeURIRef' uri] Nothing Nothing >>= \case
Right _ -> pure $ Right ()
Left e -> pure $ Left $ prettyShow e
settings' :: IORef AppState settings' :: IORef AppState
{-# NOINLINE settings' #-} {-# NOINLINE settings' #-}