Merge remote-tracking branch 'origin/pr/850'

This commit is contained in:
Julian Ospald 2023-12-02 18:42:22 +08:00
commit e214695a3e
No known key found for this signature in database
GPG Key ID: 4275CDA6A29BED43
2 changed files with 560 additions and 246 deletions

View File

@ -7,6 +7,10 @@
{-# LANGUAGE ViewPatterns #-} {-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-unused-record-wildcards #-} {-# OPTIONS_GHC -Wno-unused-record-wildcards #-}
{-# OPTIONS_GHC -Wno-unused-matches #-} {-# OPTIONS_GHC -Wno-unused-matches #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BangPatterns #-}
module BrickMain where module BrickMain where
@ -23,13 +27,23 @@ import GHCup.Prelude.Process
import GHCup.Prompts import GHCup.Prompts
import Brick import Brick
import Brick.Widgets.Border ( BrickEvent(VtyEvent, MouseDown),
import Brick.Widgets.Border.Style App(..),
import Brick.Widgets.Center Padding(Max, Pad),
import Brick.Widgets.List ( listSelectedFocusedAttr AttrMap,
, listSelectedAttr EventM,
, listAttr Size(..),
) Widget(..),
ViewportType (Vertical),
(<+>),
(<=>))
import qualified Brick
import Brick.Widgets.Border ( hBorder, borderWithLabel)
import Brick.Widgets.Border.Style ( unicode )
import Brick.Widgets.Center ( center, centerLayer )
import qualified Brick.Widgets.List as L
import Brick.Focus (FocusRing)
import qualified Brick.Focus as F
import Codec.Archive import Codec.Archive
import Control.Applicative import Control.Applicative
import Control.Exception.Safe import Control.Exception.Safe
@ -41,13 +55,14 @@ import Control.Monad.Trans.Except
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import Data.Bool import Data.Bool
import Data.Functor import Data.Functor
import Data.Function ( (&), on)
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import Data.IORef import Data.IORef (IORef, readIORef, newIORef, writeIORef, modifyIORef)
import Data.Vector ( Vector import Data.Vector ( Vector
, (!?)
) )
import Data.Versions import Data.Versions hiding (Lens')
import Haskus.Utils.Variant.Excepts import Haskus.Utils.Variant.Excepts
import Prelude hiding ( appendFile ) import Prelude hiding ( appendFile )
import System.Exit import System.Exit
@ -68,6 +83,175 @@ import System.FilePath
import qualified System.Posix.Process as SPP import qualified System.Posix.Process as SPP
#endif #endif
import Optics.TH (makeLenses, makeLensesFor)
import Optics.State (use)
import Optics.State.Operators ( (.=), (%=), (<%=))
import Optics.Operators ((.~), (^.), (%~))
import Optics.Getter (view)
import Optics.Lens (Lens', lens, toLensVL)
{- Brick's widget:
It is a FocusRing over many list's. Each list contains the information for each tool. Each list has an internal name (for Brick's runtime)
and a label which we can use in rendering. This data-structure helps to reuse Brick.Widget.List and to navegate easily across
Consider this code as private. GenericSectionList should not be used directly as the FocusRing should be align with the Vector containing
the elements, otherwise you'd be focusing on a non-existent widget with unknown result (In theory the code is safe unless you have an empty section list).
- To build a SectionList use the safe constructor sectionList
- To access sections use the lens provider sectionL and the name of the section you'd like to access
- You can modify Brick.Widget.List.GenericList within GenericSectionList via sectionL but do not
modify the vector length
-}
data GenericSectionList n t e
= GenericSectionList
{ sectionListFocusRing :: FocusRing n -- ^ The FocusRing for all sections
, sectionListElements :: !(Vector (L.GenericList n t e)) -- ^ A key-value vector
, sectionListName :: n -- ^ The section list name
}
makeLensesFor [("sectionListFocusRing", "sectionListFocusRingL"), ("sectionListElements", "sectionListElementsL"), ("sectionListName", "sectionListNameL")] ''GenericSectionList
type SectionList n e = GenericSectionList n V.Vector e
-- | Build a SectionList from nonempty list. If empty we could not defined sectionL lenses.
sectionList :: Foldable t
=> n -- The name of the section list
-> [(n, t e)] -- a list of tuples (section name, collection of elements)
-> Int
-> GenericSectionList n t e
sectionList name elements height
= GenericSectionList
{ sectionListFocusRing = F.focusRing [section_name | (section_name, _) <- elements]
, sectionListElements = V.fromList [L.list section_name els height | (section_name, els) <- elements]
, sectionListName = name
}
-- | This lens constructor, takes a name and looks if a section has such a name.
-- Used to dispatch events to sections. It is a partial function only meant to
-- be used with the FocusRing inside GenericSectionList
sectionL :: Eq n => n -> Lens' (GenericSectionList n t e) (L.GenericList n t e)
sectionL section_name = lens g s
where is_section_name = (== section_name) . L.listName
g section_list =
let elms = section_list ^. sectionListElementsL
zeroth = elms V.! 0 -- TODO: This crashes for empty vectors.
in fromMaybe zeroth (V.find is_section_name elms)
s gl@(GenericSectionList _ elms _) list =
case V.findIndex is_section_name elms of
Nothing -> gl
Just i -> let new_elms = V.update elms (V.fromList [(i, list)])
in gl & sectionListElementsL .~ new_elms
moveDown :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()
moveDown = do
ring <- use sectionListFocusRingL
case F.focusGetCurrent ring of
Nothing -> pure ()
Just l -> do -- If it is the last element, move to the first element of the next focus; else, just handle regular list event.
current_list <- use (sectionL l)
let current_idx = L.listSelected current_list
list_length = current_list & length
if current_idx == Just (list_length - 1)
then do
new_focus <- sectionListFocusRingL <%= F.focusNext
case F.focusGetCurrent new_focus of
Nothing -> pure () -- |- Optic.Zoom.zoom doesn't typecheck but Lens.Micro.Mtl.zoom does. It is re-exported by Brick
Just new_l -> Brick.zoom (toLensVL $ sectionL new_l) (Brick.modify L.listMoveToBeginning)
else Brick.zoom (toLensVL $ sectionL l) $ Brick.modify L.listMoveDown
moveUp :: (L.Splittable t, Ord n, Foldable t) => EventM n (GenericSectionList n t e) ()
moveUp = do
ring <- use sectionListFocusRingL
case F.focusGetCurrent ring of
Nothing -> pure ()
Just l -> do -- If it is the first element, move to the last element of the prev focus; else, just handle regular list event.
current_list <- use (sectionL l)
let current_idx = L.listSelected current_list
if current_idx == Just 0
then do
new_focus <- sectionListFocusRingL <%= F.focusPrev
case F.focusGetCurrent new_focus of
Nothing -> pure ()
Just new_l -> Brick.zoom (toLensVL $ sectionL new_l) (Brick.modify L.listMoveToEnd)
else Brick.zoom (toLensVL $ sectionL l) $ Brick.modify L.listMoveUp
-- | Handle events for list cursor movement. Events handled are:
--
-- * Up (up arrow key). If first element of section, then jump prev section
-- * Down (down arrow key). If last element of section, then jump next section
-- * Page Up (PgUp)
-- * Page Down (PgDown)
-- * Go to next section (Tab)
-- * Go to prev section (BackTab)
handleGenericListEvent :: (Foldable t, L.Splittable t, Ord n)
=> BrickEvent n a
-> EventM n (GenericSectionList n t e) ()
handleGenericListEvent (VtyEvent (Vty.EvResize _ _)) = pure ()
handleGenericListEvent (VtyEvent (Vty.EvKey (Vty.KChar '\t') [])) = sectionListFocusRingL %= F.focusNext
handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KBackTab [])) = sectionListFocusRingL %= F.focusPrev
handleGenericListEvent (MouseDown _ Vty.BScrollDown _ _) = moveDown
handleGenericListEvent (MouseDown _ Vty.BScrollUp _ _) = moveUp
handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KDown [])) = moveDown
handleGenericListEvent (VtyEvent (Vty.EvKey Vty.KUp [])) = moveUp
handleGenericListEvent (VtyEvent ev) = do
ring <- use sectionListFocusRingL
case F.focusGetCurrent ring of
Nothing -> pure ()
Just l -> Brick.zoom (toLensVL $ sectionL l) $ L.handleListEvent ev
handleGenericListEvent _ = pure ()
-- This re-uses Brick.Widget.List.renderList
renderSectionList :: (Traversable t, Ord n, Show n, Eq n, L.Splittable t)
=> (Bool -> e -> Widget n) -- ^ Rendering function of the list element, True for the selected element
-> Bool -- ^ Whether the section list has focus
-> GenericSectionList n t e -- ^ The section list to render
-> Widget n
renderSectionList render_elem section_focus (GenericSectionList focus elms sl_name) =
Brick.Widget Brick.Greedy Brick.Greedy $ do
c <- Brick.getContext
let -- A section is focused if the whole thing is focused, and the inner list has focus
section_is_focused l = section_focus && (Just (L.listName l) == F.focusGetCurrent focus)
-- We need to limit the widget size when the length of the list is higher than the size of the terminal
limit = min (Brick.windowHeight c) (Brick.availHeight c)
s_idx = fromMaybe 0 $ V.findIndex section_is_focused elms
render_inner_list has_focus l = Brick.vLimit (length l) $ L.renderList (\b -> render_elem (b && has_focus)) has_focus l
(widget, off) =
V.ifoldl' (\wacc i list ->
let has_focus_list = section_is_focused list
(!acc_widget, !acc_off) = wacc
new_widget = if i == 0 then render_inner_list has_focus_list list else hBorder <=> render_inner_list has_focus_list list
new_off
| i < s_idx = 1 + L.listItemHeight list * length list
| i == s_idx = 1 + L.listItemHeight list * fromMaybe 0 (L.listSelected list)
| otherwise = 0
in (acc_widget <=> new_widget, acc_off + new_off)
)
(Brick.emptyWidget, 0)
elms
Brick.render $ Brick.viewport sl_name Brick.Vertical $ Brick.translateBy (Brick.Location (0, min 0 (limit-off))) widget
-- | Equivalent to listSelectedElement
sectionListSelectedElement :: (Eq n, L.Splittable t, Traversable t, Semigroup (t e)) => GenericSectionList n t e -> Maybe (Int, e)
sectionListSelectedElement generic_section_list = do
current_focus <- generic_section_list ^. sectionListFocusRingL & F.focusGetCurrent
let current_section = generic_section_list ^. sectionL current_focus
L.listSelectedElement current_section
{- Brick app data structures.
In this section we define the state, the widgets and the core data structures which we will be using for the brick app.
-}
data Name = AllTools -- The main list widget
| Singular Tool -- The particular list for each tool
| KeyInfoBox -- The text box widget with action informacion
| TutorialBox -- The tutorial widget
deriving (Eq, Ord, Show)
data Mode = Navigation | KeyInfo | Tutorial deriving (Eq, Show, Ord)
installedSign :: String installedSign :: String
#if IS_WINDOWS #if IS_WINDOWS
@ -92,56 +276,45 @@ notInstalledSign = "✗ "
data BrickData = BrickData data BrickData = BrickData
{ lr :: [ListResult] { _lr :: [ListResult]
} }
deriving Show deriving Show
data BrickSettings = BrickSettings makeLenses ''BrickData
{ showAllVersions :: Bool
}
deriving Show
data BrickInternalState = BrickInternalState data BrickSettings = BrickSettings { _showAllVersions :: Bool}
{ clr :: Vector ListResult --deriving Show
, ix :: Int
} makeLenses ''BrickSettings
deriving Show
type BrickInternalState = SectionList Name ListResult
data BrickState = BrickState data BrickState = BrickState
{ appData :: BrickData { _appData :: BrickData
, appSettings :: BrickSettings , _appSettings :: BrickSettings
, appState :: BrickInternalState , _appState :: BrickInternalState
, appKeys :: KeyBindings , _appKeys :: KeyBindings
, _mode :: Mode
}
--deriving Show
makeLenses ''BrickState
app :: AttrMap -> AttrMap -> App BrickState () Name
app attrs dimAttrs =
App { appDraw = drawUI dimAttrs
, appHandleEvent = eventHandler
, appStartEvent = return ()
, appAttrMap = const attrs
, appChooseCursor = Brick.showFirstCursor
} }
deriving Show
keyHandlers :: KeyBindings {- Drawing.
-> [ ( KeyCombination
, BrickSettings -> String
, BrickState -> EventM String BrickState ()
)
]
keyHandlers KeyBindings {..} =
[ (bQuit, const "Quit" , \_ -> halt)
, (bInstall, const "Install" , withIOAction install')
, (bUninstall, const "Uninstall", withIOAction del')
, (bSet, const "Set" , withIOAction set')
, (bChangelog, const "ChangeLog", withIOAction changelog')
, ( bShowAllVersions
, \BrickSettings {..} ->
if showAllVersions then "Don't show all versions" else "Show all versions"
, hideShowHandler (not . showAllVersions)
)
, (bUp, const "Up", \BrickState {..} -> put BrickState{ appState = moveCursor 1 appState Up, .. })
, (bDown, const "Down", \BrickState {..} -> put BrickState{ appState = moveCursor 1 appState Down, .. })
]
where
hideShowHandler f BrickState{..} =
let newAppSettings = appSettings { showAllVersions = f appSettings }
newInternalState = constructList appData newAppSettings (Just appState)
in put (BrickState appData newAppSettings newInternalState appKeys)
The section for creating our widgets.
-}
showKey :: Vty.Key -> String showKey :: Vty.Key -> String
showKey (Vty.KChar c) = [c] showKey (Vty.KChar c) = [c]
@ -153,38 +326,38 @@ showMod :: Vty.Modifier -> String
showMod = tail . show showMod = tail . show
ui :: AttrMap -> BrickState -> Widget String drawNavigation :: AttrMap -> BrickState -> Widget Name
ui dimAttrs BrickState{ appSettings = as@BrickSettings{}, ..} drawNavigation dimAttrs BrickState{ _appSettings = as@BrickSettings{}, ..}
= padBottom Max = Brick.padBottom Max
( withBorderStyle unicode ( Brick.withBorderStyle unicode
$ borderWithLabel (str "GHCup") $ borderWithLabel (Brick.str "GHCup")
(center (header <=> hBorder <=> renderList' appState)) (center (header <=> hBorder <=> renderList' _appState))
) )
<=> footer <=> footer
where where
footer = footer =
withAttr (attrName "help") Brick.withAttr helpAttr
. txtWrap . Brick.txtWrap
. T.pack . T.pack
. foldr1 (\x y -> x <> " " <> y) . foldr1 (\x y -> x <> " " <> y)
. fmap (\(KeyCombination key mods, s, _) -> intercalate "+" (showKey key : (showMod <$> mods)) <> ":" <> s as) . fmap (\(KeyCombination key mods, s, _) -> intercalate "+" (showKey key : (showMod <$> mods)) <> ":" <> s as)
$ keyHandlers appKeys $ keyHandlers _appKeys
header = header =
minHSize 2 emptyWidget minHSize 2 Brick.emptyWidget
<+> padLeft (Pad 2) (minHSize 6 $ str "Tool") <+> Brick.padLeft (Pad 2) (minHSize 6 $ Brick.str "Tool")
<+> minHSize 15 (str "Version") <+> minHSize 15 (Brick.str "Version")
<+> padLeft (Pad 1) (minHSize 25 $ str "Tags") <+> Brick.padLeft (Pad 1) (minHSize 25 $ Brick.str "Tags")
<+> padLeft (Pad 5) (str "Notes") <+> Brick.padLeft (Pad 5) (Brick.str "Notes")
renderList' bis@BrickInternalState{..} = renderList' bis =
let minTagSize = V.maximum $ V.map (length . intercalate "," . fmap tagToString . lTag) clr let allElements = V.concatMap L.listElements $ sectionListElements bis
minVerSize = V.maximum $ V.map (\ListResult{..} -> T.length $ tVerToText (GHCTargetVersion lCross lVer)) clr minTagSize = V.maximum $ V.map (length . intercalate "," . fmap tagToString . lTag) allElements
in withDefAttr listAttr . drawListElements (renderItem minTagSize minVerSize) True $ bis minVerSize = V.maximum $ V.map (\ListResult{..} -> T.length $ tVerToText (GHCTargetVersion lCross lVer)) allElements
renderItem minTagSize minVerSize _ b listResult@ListResult{lTag = lTag', ..} = in Brick.withDefAttr L.listAttr $ renderSectionList (renderItem minTagSize minVerSize) True bis
renderItem minTagSize minVerSize b listResult@ListResult{lTag = lTag', ..} =
let marks = if let marks = if
| lSet -> (withAttr (attrName "set") $ str setSign) | lSet -> (Brick.withAttr setAttr $ Brick.str setSign)
| lInstalled -> (withAttr (attrName "installed") $ str installedSign) | lInstalled -> (Brick.withAttr installedAttr $ Brick.str installedSign)
| otherwise -> (withAttr (attrName "not-installed") $ str notInstalledSign) | otherwise -> (Brick.withAttr notInstalledAttr $ Brick.str notInstalledSign)
ver = case lCross of ver = case lCross of
Nothing -> T.unpack . prettyVer $ lVer Nothing -> T.unpack . prettyVer $ lVer
Just c -> T.unpack (c <> "-" <> prettyVer lVer) Just c -> T.unpack (c <> "-" <> prettyVer lVer)
@ -192,127 +365,192 @@ ui dimAttrs BrickState{ appSettings = as@BrickSettings{}, ..}
| lNoBindist && not lInstalled | lNoBindist && not lInstalled
&& not b -- TODO: overloading dim and active ignores active && not b -- TODO: overloading dim and active ignores active
-- so we hack around it here -- so we hack around it here
= updateAttrMap (const dimAttrs) . withAttr (attrName "no-bindist") = Brick.updateAttrMap (const dimAttrs) . Brick.withAttr (Brick.attrName "no-bindist")
| otherwise = id | otherwise = id
hooray hooray
| elem Latest lTag' && not lInstalled = | elem Latest lTag' && not lInstalled =
withAttr (attrName "hooray") Brick.withAttr hoorayAttr
| otherwise = id | otherwise = id
active = if b then putCursor "GHCup" (Location (0,0)) . forceAttr (attrName "active") else id in hooray $ dim
in hooray $ active $ dim
( marks ( marks
<+> padLeft (Pad 2) <+> Brick.padLeft (Pad 2)
( minHSize 6 ( minHSize 6
(printTool lTool) (printTool lTool)
) )
<+> minHSize minVerSize (str ver) <+> minHSize minVerSize (Brick.str ver)
<+> (let l = catMaybes . fmap printTag $ sort lTag' <+> (let l = catMaybes . fmap printTag $ sort lTag'
in padLeft (Pad 1) $ minHSize minTagSize $ if null l in Brick.padLeft (Pad 1) $ minHSize minTagSize $ if null l
then emptyWidget then Brick.emptyWidget
else foldr1 (\x y -> x <+> str "," <+> y) l else foldr1 (\x y -> x <+> Brick.str "," <+> y) l
) )
<+> padLeft (Pad 5) <+> Brick.padLeft (Pad 5)
( let notes = printNotes listResult ( let notes = printNotes listResult
in if null notes in if null notes
then emptyWidget then Brick.emptyWidget
else foldr1 (\x y -> x <+> str "," <+> y) notes else foldr1 (\x y -> x <+> Brick.str "," <+> y) notes
) )
<+> vLimit 1 (fill ' ') <+> Brick.vLimit 1 (Brick.fill ' ')
) )
printTag Recommended = Just $ withAttr (attrName "recommended") $ str "recommended" printTag Recommended = Just $ Brick.withAttr recommendedAttr $ Brick.str "recommended"
printTag Latest = Just $ withAttr (attrName "latest") $ str "latest" printTag Latest = Just $ Brick.withAttr latestAttr $ Brick.str "latest"
printTag Prerelease = Just $ withAttr (attrName "prerelease") $ str "prerelease" printTag Prerelease = Just $ Brick.withAttr prereleaseAttr $ Brick.str "prerelease"
printTag Nightly = Just $ withAttr (attrName "nightly") $ str "nightly" printTag Nightly = Just $ Brick.withAttr nightlyAttr $ Brick.str "nightly"
printTag (Base pvp'') = Just $ str ("base-" ++ T.unpack (prettyPVP pvp'')) printTag (Base pvp'') = Just $ Brick.str ("base-" ++ T.unpack (prettyPVP pvp''))
printTag Old = Nothing printTag Old = Nothing
printTag LatestPrerelease = Just $ withAttr (attrName "latest-prerelease") $ str "latest-prerelease" printTag LatestPrerelease = Just $ Brick.withAttr latestPrereleaseAttr $ Brick.str "latest-prerelease"
printTag LatestNightly = Just $ withAttr (attrName "latest-nightly") $ str "latest-nightly" printTag LatestNightly = Just $ Brick.withAttr latestNightlyAttr $ Brick.str "latest-nightly"
printTag (UnknownTag t) = Just $ str t printTag (UnknownTag t) = Just $ Brick.str t
printTool Cabal = str "cabal" printTool Cabal = Brick.str "cabal"
printTool GHC = str "GHC" printTool GHC = Brick.str "GHC"
printTool GHCup = str "GHCup" printTool GHCup = Brick.str "GHCup"
printTool HLS = str "HLS" printTool HLS = Brick.str "HLS"
printTool Stack = str "Stack" printTool Stack = Brick.str "Stack"
printNotes ListResult {..} = printNotes ListResult {..} =
(if hlsPowered then [withAttr (attrName "hls-powered") $ str "hls-powered"] else mempty (if hlsPowered then [Brick.withAttr hlsPoweredAttr $ Brick.str "hls-powered"] else mempty
) )
++ (if lStray then [withAttr (attrName "stray") $ str "stray"] else mempty) ++ (if lStray then [Brick.withAttr strayAttr $ Brick.str "stray"] else mempty)
++ (case lReleaseDay of ++ (case lReleaseDay of
Nothing -> mempty Nothing -> mempty
Just d -> [withAttr (attrName "day") $ str (show d)]) Just d -> [Brick.withAttr dayAttr $ Brick.str (show d)])
-- | Draws the list elements. minHSize s' = Brick.hLimit s' . Brick.vLimit 1 . (<+> Brick.fill ' ')
--
-- Evaluates the underlying container up to, and a bit beyond, the drawTutorial :: Widget Name
-- selected element. The exact amount depends on available height drawTutorial =
-- for drawing and 'listItemHeight'. At most, it will evaluate up to
-- element @(i + h + 1)@ where @i@ is the selected index and @h@ is the
-- available height.
drawListElements :: (Int -> Bool -> ListResult -> Widget String)
-> Bool
-> BrickInternalState
-> Widget String
drawListElements drawElem foc is@(BrickInternalState clr _) =
Widget Greedy Greedy $
let let
es = clr mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)
listSelected = fmap fst $ listSelectedElement' is txt_separator = hBorder <+> Brick.str " o " <+> hBorder
in centerLayer
$ Brick.hLimitPercent 75
$ Brick.vLimitPercent 50
$ Brick.withBorderStyle unicode
$ borderWithLabel (Brick.txt "Tutorial")
$ Brick.vBox
(fmap center
[ mkTextBox [Brick.txtWrap "GHCup is a distribution channel for Haskell's tools."]
, txt_separator
, mkTextBox [
Brick.hBox [
Brick.txt "This symbol "
, Brick.withAttr installedAttr (Brick.str installedSign)
, Brick.txtWrap " means that the tool is installed but not in used"
]
, Brick.hBox [
Brick.txt "This symbol "
, Brick.withAttr setAttr (Brick.str setSign)
, Brick.txtWrap " means that the tool is installed and in used"
]
, Brick.hBox [
Brick.txt "This symbol "
, Brick.withAttr notInstalledAttr (Brick.str notInstalledSign)
, Brick.txt " means that the tool isn't installed"
]
]
, txt_separator
, mkTextBox [
Brick.hBox [
Brick.withAttr recommendedAttr $ Brick.str "recommended"
, Brick.txtWrap " tag is based on community adoption, known bugs, etc... So It makes this version the least experimental"
]
, Brick.hBox [
Brick.withAttr latestAttr $ Brick.str "latest"
, Brick.txtWrap " tag is for the latest distributed version of the tool"
]
, Brick.hBox [
Brick.withAttr latestAttr $ Brick.str "hls-powered"
, Brick.txt " denotes the compiler version supported by the currently set ("
, Brick.withAttr setAttr (Brick.str setSign)
, Brick.txt ") hls"
]
, Brick.txtWrap "base-X.Y.Z.W tag is the minimun version of the base package admited in such ghc version"
]
, Brick.txt " "
])
<=> Brick.padRight Brick.Max (Brick.txt "Press q to exit the tutorial")
drawnElements = flip V.imap es $ \i' e -> drawKeyInfo :: KeyBindings -> Widget Name
let addSeparator w = case es !? (i' - 1) of drawKeyInfo KeyBindings {..} =
Just e' | lTool e' /= lTool e -> let
hBorder <=> w mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max)
_ -> w keyToWidget (KeyCombination key mods) = Brick.str $ intercalate "+" (showKey key : (showMod <$> mods))
in centerLayer
$ Brick.hLimitPercent 75
$ Brick.vLimitPercent 50
$ Brick.withBorderStyle unicode
$ borderWithLabel (Brick.txt "Key Actions")
$ Brick.vBox [
center $
mkTextBox [
Brick.hBox [
Brick.txt "Press "
, keyToWidget bUp, Brick.txt " and ", keyToWidget bDown
, Brick.txtWrap " to navigate the list of tools"
]
, Brick.hBox [
Brick.txt "Press "
, keyToWidget bInstall
, Brick.txtWrap " to install the selected tool. Notice, you may need to set it as default afterwards"
]
, Brick.hBox [
Brick.txt "Press "
, keyToWidget bSet
, Brick.txtWrap " to set a tool as the one for use"
]
, Brick.hBox [
Brick.txt "Press "
, keyToWidget bUninstall
, Brick.txtWrap " to uninstall a tool"
]
, Brick.hBox [
Brick.txt "Press "
, keyToWidget bChangelog
, Brick.txtWrap " to open the tool's changelog. It will open a web browser"
]
, Brick.hBox [
Brick.txt "Press "
, keyToWidget bShowAllVersions
, Brick.txtWrap " to show older version of each tool"
]
]
]
<=> Brick.hBox [Brick.txt "Press q to return to Navigation" <+> Brick.padRight Brick.Max (Brick.txt " ") <+> Brick.txt "Press Enter to go to the Tutorial"]
isSelected = Just i' == listSelected drawUI :: AttrMap -> BrickState -> [Widget Name]
elemWidget = drawElem i' isSelected e drawUI dimAttrs st =
selItemAttr = if foc let navg = drawNavigation dimAttrs st
then withDefAttr listSelectedFocusedAttr in case st ^. mode of
else withDefAttr listSelectedAttr Navigation -> [navg]
makeVisible' = if isSelected then visible . selItemAttr else id Tutorial -> [drawTutorial, navg]
in addSeparator $ makeVisible' elemWidget KeyInfo -> [drawKeyInfo (st ^. appKeys), navg]
in render {- Attributes
$ viewport "GHCup" Vertical
$ vBox
$ V.toList drawnElements
-}
minHSize :: Int -> Widget n -> Widget n
minHSize s' = hLimit s' . vLimit 1 . (<+> fill ' ')
app :: AttrMap -> AttrMap -> App BrickState e String
app attrs dimAttrs =
App { appDraw = \st -> [ui dimAttrs st]
, appHandleEvent = \be -> get >>= \s -> eventHandler s be
, appStartEvent = return ()
, appAttrMap = const attrs
, appChooseCursor = showFirstCursor
}
defaultAttributes :: Bool -> AttrMap defaultAttributes :: Bool -> AttrMap
defaultAttributes no_color = attrMap defaultAttributes no_color = Brick.attrMap
Vty.defAttr Vty.defAttr
[ (attrName "active" , Vty.defAttr `withBackColor` Vty.blue) [ (L.listSelectedFocusedAttr , Vty.defAttr `withBackColor` Vty.blue)
, (attrName "not-installed" , Vty.defAttr `withForeColor` Vty.red) , (L.listSelectedAttr , Vty.defAttr)
, (attrName "set" , Vty.defAttr `withForeColor` Vty.green) , (notInstalledAttr , Vty.defAttr `withForeColor` Vty.red)
, (attrName "installed" , Vty.defAttr `withForeColor` Vty.green) , (setAttr , Vty.defAttr `withForeColor` Vty.green)
, (attrName "recommended" , Vty.defAttr `withForeColor` Vty.green) , (installedAttr , Vty.defAttr `withForeColor` Vty.green)
, (attrName "hls-powered" , Vty.defAttr `withForeColor` Vty.green) , (recommendedAttr , Vty.defAttr `withForeColor` Vty.green)
, (attrName "latest" , Vty.defAttr `withForeColor` Vty.yellow) , (hlsPoweredAttr , Vty.defAttr `withForeColor` Vty.green)
, (attrName "latest-prerelease" , Vty.defAttr `withForeColor` Vty.red) , (latestAttr , Vty.defAttr `withForeColor` Vty.yellow)
, (attrName "latest-nightly" , Vty.defAttr `withForeColor` Vty.red) , (latestPrereleaseAttr , Vty.defAttr `withForeColor` Vty.red)
, (attrName "prerelease" , Vty.defAttr `withForeColor` Vty.red) , (latestNightlyAttr , Vty.defAttr `withForeColor` Vty.red)
, (attrName "nightly" , Vty.defAttr `withForeColor` Vty.red) , (prereleaseAttr , Vty.defAttr `withForeColor` Vty.red)
, (attrName "compiled" , Vty.defAttr `withForeColor` Vty.blue) , (nightlyAttr , Vty.defAttr `withForeColor` Vty.red)
, (attrName "stray" , Vty.defAttr `withForeColor` Vty.blue) , (compiledAttr , Vty.defAttr `withForeColor` Vty.blue)
, (attrName "day" , Vty.defAttr `withForeColor` Vty.blue) , (strayAttr , Vty.defAttr `withForeColor` Vty.blue)
, (attrName "help" , Vty.defAttr `withStyle` Vty.italic) , (dayAttr , Vty.defAttr `withForeColor` Vty.blue)
, (attrName "hooray" , Vty.defAttr `withForeColor` Vty.brightWhite) , (helpAttr , Vty.defAttr `withStyle` Vty.italic)
, (hoorayAttr , Vty.defAttr `withForeColor` Vty.brightWhite)
] ]
where where
withForeColor | no_color = const withForeColor | no_color = const
@ -323,58 +561,130 @@ defaultAttributes no_color = attrMap
withStyle = Vty.withStyle withStyle = Vty.withStyle
notInstalledAttr, setAttr, installedAttr, recommendedAttr, hlsPoweredAttr:: Brick.AttrName
latestAttr, latestPrereleaseAttr, latestNightlyAttr, prereleaseAttr, nightlyAttr:: Brick.AttrName
compiledAttr, strayAttr, dayAttr, helpAttr, hoorayAttr:: Brick.AttrName
notInstalledAttr = Brick.attrName "not-installed"
setAttr = Brick.attrName "set"
installedAttr = Brick.attrName "installed"
recommendedAttr = Brick.attrName "recommended"
hlsPoweredAttr = Brick.attrName "hls-powered"
latestAttr = Brick.attrName "latest"
latestPrereleaseAttr = Brick.attrName "latest-prerelease"
latestNightlyAttr = Brick.attrName "latest-nightly"
prereleaseAttr = Brick.attrName "prerelease"
nightlyAttr = Brick.attrName "nightly"
compiledAttr = Brick.attrName "compiled"
strayAttr = Brick.attrName "stray"
dayAttr = Brick.attrName "day"
helpAttr = Brick.attrName "help"
hoorayAttr = Brick.attrName "hooray"
dimAttributes :: Bool -> AttrMap dimAttributes :: Bool -> AttrMap
dimAttributes no_color = attrMap dimAttributes no_color = Brick.attrMap
(Vty.defAttr `Vty.withStyle` Vty.dim) (Vty.defAttr `Vty.withStyle` Vty.dim)
[ (attrName "active" , Vty.defAttr `withBackColor` Vty.blue) -- has no effect ?? [ (Brick.attrName "active" , Vty.defAttr `withBackColor` Vty.blue) -- has no effect ??
, (attrName "no-bindist", Vty.defAttr `Vty.withStyle` Vty.dim) , (Brick.attrName "no-bindist", Vty.defAttr `Vty.withStyle` Vty.dim)
] ]
where where
withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo withBackColor | no_color = \attr _ -> attr `Vty.withStyle` Vty.reverseVideo
| otherwise = Vty.withBackColor | otherwise = Vty.withBackColor
eventHandler :: BrickState -> BrickEvent String e -> EventM String BrickState () {- Handlers
eventHandler st@BrickState{..} ev = do
-}
keyHandlers :: KeyBindings
-> [ ( KeyCombination
, BrickSettings -> String
, EventM Name BrickState ()
)
]
keyHandlers KeyBindings {..} =
[ (bQuit, const "Quit" , Brick.halt)
, (bInstall, const "Install" , withIOAction install')
, (bUninstall, const "Uninstall", withIOAction del')
, (bSet, const "Set" , withIOAction set')
, (bChangelog, const "ChangeLog", withIOAction changelog')
, ( bShowAllVersions
, \BrickSettings {..} ->
if _showAllVersions then "Don't show all versions" else "Show all versions"
, hideShowHandler' (not . _showAllVersions)
)
, (bUp, const "Up", Brick.zoom (toLensVL appState) moveUp)
, (bDown, const "Down", Brick.zoom (toLensVL appState) moveDown)
, (KeyCombination (Vty.KChar 'h') [], const "help", mode .= KeyInfo)
]
where
--hideShowHandler' :: (BrickSettings -> Bool) -> (BrickSettings -> Bool) -> m ()
hideShowHandler' f = do
app_settings <- use appSettings
let
vers = f app_settings
newAppSettings = app_settings & showAllVersions .~ vers
ad <- use appData
current_app_state <- use appState
appSettings .= newAppSettings
appState .= constructList ad newAppSettings (Just current_app_state)
tutorialHandler :: BrickEvent Name e -> EventM Name BrickState ()
tutorialHandler ev =
case ev of
VtyEvent (Vty.EvKey (Vty.KChar 'q') _ ) -> mode .= Navigation
_ -> pure ()
keyInfoHandler :: BrickEvent Name e -> EventM Name BrickState ()
keyInfoHandler ev = do
case ev of
VtyEvent (Vty.EvKey (Vty.KChar 'q') _ ) -> mode .= Navigation
VtyEvent (Vty.EvKey Vty.KEnter _ ) -> mode .= Tutorial
_ -> pure ()
navigationHandler :: BrickEvent Name e -> EventM Name BrickState ()
navigationHandler ev = do
AppState { keyBindings = kb } <- liftIO $ readIORef settings' AppState { keyBindings = kb } <- liftIO $ readIORef settings'
case ev of case ev of
(MouseDown _ Vty.BScrollUp _ _) -> inner_event@(VtyEvent (Vty.EvKey key _)) ->
put (BrickState { appState = moveCursor 1 appState Up, .. }) case find (\(key', _, _) -> key' == KeyCombination key []) (keyHandlers kb) of
(MouseDown _ Vty.BScrollDown _ _) -> Nothing -> void $ Brick.zoom (toLensVL appState) $ handleGenericListEvent inner_event
put (BrickState { appState = moveCursor 1 appState Down, .. }) Just (_, _, handler) -> handler
(VtyEvent (Vty.EvResize _ _)) -> put st inner_event -> Brick.zoom (toLensVL appState) $ handleGenericListEvent inner_event
(VtyEvent (Vty.EvKey Vty.KUp [])) ->
put BrickState{ appState = moveCursor 1 appState Up, .. } eventHandler :: BrickEvent Name e -> EventM Name BrickState ()
(VtyEvent (Vty.EvKey Vty.KDown [])) -> eventHandler ev = do
put BrickState{ appState = moveCursor 1 appState Down, .. } m <- use mode
(VtyEvent (Vty.EvKey key mods)) -> case m of
case find (\(keyCombo, _, _) -> keyCombo == KeyCombination key mods) (keyHandlers kb) of KeyInfo -> keyInfoHandler ev
Nothing -> put st Tutorial -> tutorialHandler ev
Just (_, _, handler) -> handler st Navigation -> navigationHandler ev
_ -> put st
moveCursor :: Int -> BrickInternalState -> Direction -> BrickInternalState {- Core Logic.
moveCursor steps ais@BrickInternalState{..} direction =
let newIx = if direction == Down then ix + steps else ix - steps
in case clr !? newIx of
Just _ -> BrickInternalState { ix = newIx, .. }
Nothing -> ais
This section defines the IO actions we can execute within the Brick App:
- Install
- Set
- UnInstall
- Launch the Changelog
-}
-- | Suspend the current UI and run an IO action in terminal. If the -- | Suspend the current UI and run an IO action in terminal. If the
-- IO action returns a Left value, then it's thrown as userError. -- IO action returns a Left value, then it's thrown as userError.
withIOAction :: Ord n withIOAction :: (Ord n, Eq n)
=> (BrickState => ( (Int, ListResult) -> ReaderT AppState IO (Either String a))
-> (Int, ListResult)
-> ReaderT AppState IO (Either String a))
-> BrickState
-> EventM n BrickState () -> EventM n BrickState ()
withIOAction action as = case listSelectedElement' (appState as) of withIOAction action = do
Nothing -> put as as <- Brick.get
Just (ix, e) -> do case sectionListSelectedElement (view appState as) of
suspendAndResume $ do Nothing -> pure ()
Just (curr_ix, e) -> do
Brick.suspendAndResume $ do
settings <- readIORef settings' settings <- readIORef settings'
flip runReaderT settings $ action as (ix, e) >>= \case flip runReaderT settings $ action (curr_ix, e) >>= \case
Left err -> liftIO $ putStrLn ("Error: " <> err) Left err -> liftIO $ putStrLn ("Error: " <> err)
Right _ -> liftIO $ putStrLn "Success" Right _ -> liftIO $ putStrLn "Success"
getAppData Nothing >>= \case getAppData Nothing >>= \case
@ -390,28 +700,35 @@ withIOAction action as = case listSelectedElement' (appState as) of
-- and @BrickSettings@. -- and @BrickSettings@.
updateList :: BrickData -> BrickState -> BrickState updateList :: BrickData -> BrickState -> BrickState
updateList appD BrickState{..} = updateList appD BrickState{..} =
let newInternalState = constructList appD appSettings (Just appState) let newInternalState = constructList appD _appSettings (Just _appState)
in BrickState { appState = newInternalState in BrickState { _appState = newInternalState
, appData = appD , _appData = appD
, appSettings = appSettings , _appSettings = _appSettings
, appKeys = appKeys , _appKeys = _appKeys
, _mode = Navigation
} }
constructList :: BrickData constructList :: BrickData
-> BrickSettings -> BrickSettings
-> Maybe BrickInternalState -> Maybe BrickInternalState
-> BrickInternalState -> BrickInternalState
constructList appD appSettings = constructList appD settings =
replaceLR (filterVisible (showAllVersions appSettings)) replaceLR (filterVisible (_showAllVersions settings))
(lr appD) (_lr appD)
listSelectedElement' :: BrickInternalState -> Maybe (Int, ListResult) -- | Focus on the tool section and the predicate which matches. If no result matches, focus on index 0
listSelectedElement' BrickInternalState{..} = fmap (ix, ) $ clr !? ix selectBy :: Tool -> (ListResult -> Bool) -> BrickInternalState -> BrickInternalState
selectBy tool predicate internal_state =
let new_focus = F.focusSetCurrent (Singular tool) (view sectionListFocusRingL internal_state)
tool_lens = sectionL (Singular tool)
in internal_state
& sectionListFocusRingL .~ new_focus
& tool_lens %~ L.listMoveTo 0 -- We move to 0 first
& tool_lens %~ L.listFindBy predicate -- The lookup by the predicate.
-- | Select the latests GHC tool
selectLatest :: Vector ListResult -> Int selectLatest :: BrickInternalState -> BrickInternalState
selectLatest = fromMaybe 0 . V.findIndex (\ListResult {..} -> lTool == GHC && Latest `elem` lTag) selectLatest = selectBy GHC (elem Latest . lTag)
-- | Replace the @appState@ or construct it based on a filter function -- | Replace the @appState@ or construct it based on a filter function
@ -421,14 +738,13 @@ replaceLR :: (ListResult -> Bool)
-> [ListResult] -> [ListResult]
-> Maybe BrickInternalState -> Maybe BrickInternalState
-> BrickInternalState -> BrickInternalState
replaceLR filterF lr s = replaceLR filterF list_result s =
let oldElem = s >>= listSelectedElement' let oldElem = s >>= sectionListSelectedElement -- Maybe (Int, e)
newVec = V.fromList . filter filterF $ lr newVec = [(Singular $ lTool (head g), V.fromList g) | g <- groupBy ((==) `on` lTool ) (filter filterF list_result)]
newSelected = newSectionList = sectionList AllTools newVec 1
case oldElem >>= \(_, oldE) -> V.findIndex (toolEqual oldE) newVec of in case oldElem of
Just ix -> ix Just (_, el) -> selectBy (lTool el) (toolEqual el) newSectionList
Nothing -> selectLatest newVec Nothing -> selectLatest newSectionList
in BrickInternalState newVec newSelected
where where
toolEqual e1 e2 = toolEqual e1 e2 =
lTool e1 == lTool e2 && lVer e1 == lVer e2 && lCross e1 == lCross e2 lTool e1 == lTool e2 && lVer e1 == lVer e2 && lCross e1 == lCross e2
@ -446,10 +762,9 @@ filterVisible v e | lInstalled e = True
install' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m) install' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)
=> BrickState => (Int, ListResult)
-> (Int, ListResult)
-> m (Either String ()) -> m (Either String ())
install' _ (_, ListResult {..}) = do install' (_, ListResult {..}) = do
AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
let run = let run =
@ -531,10 +846,9 @@ install' _ (_, ListResult {..}) = do
set' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m) set' :: (MonadReader AppState m, MonadIO m, MonadThrow m, MonadFail m, MonadMask m, MonadUnliftIO m, Alternative m)
=> BrickState => (Int, ListResult)
-> (Int, ListResult)
-> m (Either String ()) -> m (Either String ())
set' bs input@(_, ListResult {..}) = do set' input@(_, ListResult {..}) = do
settings <- liftIO $ readIORef settings' settings <- liftIO $ readIORef settings'
let run = let run =
@ -589,12 +903,12 @@ set' bs input@(_, ListResult {..}) = do
promptAnswer <- getUserPromptResponse userPrompt promptAnswer <- getUserPromptResponse userPrompt
case promptAnswer of case promptAnswer of
PromptYes -> do PromptYes -> do
res <- install' bs input res <- install' input
case res of case res of
(Left err) -> pure $ Left err (Left err) -> pure $ Left err
(Right _) -> do (Right _) -> do
logInfo "Setting now..." logInfo "Setting now..."
set' bs input set' input
PromptNo -> pure $ Left (prettyHFError e) PromptNo -> pure $ Left (prettyHFError e)
where where
@ -609,10 +923,9 @@ set' bs input@(_, ListResult {..}) = do
del' :: (MonadReader AppState m, MonadIO m, MonadFail m, MonadMask m, MonadUnliftIO m) del' :: (MonadReader AppState m, MonadIO m, MonadFail m, MonadMask m, MonadUnliftIO m)
=> BrickState => (Int, ListResult)
-> (Int, ListResult)
-> m (Either String ()) -> m (Either String ())
del' _ (_, ListResult {..}) = do del' (_, ListResult {..}) = do
AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask AppState { ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
let run = runE @'[NotInstalled, UninstallFailed] let run = runE @'[NotInstalled, UninstallFailed]
@ -636,10 +949,9 @@ del' _ (_, ListResult {..}) = do
changelog' :: (MonadReader AppState m, MonadIO m) changelog' :: (MonadReader AppState m, MonadIO m)
=> BrickState => (Int, ListResult)
-> (Int, ListResult)
-> m (Either String ()) -> m (Either String ())
changelog' _ (_, ListResult {..}) = do changelog' (_, ListResult {..}) = do
AppState { pfreq, ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask AppState { pfreq, ghcupInfo = GHCupInfo { _ghcupDownloads = dls }} <- ask
case getChangeLog dls lTool (ToolVersion lVer) of case getChangeLog dls lTool (ToolVersion lVer) of
Nothing -> pure $ Left $ Nothing -> pure $ Left $
@ -678,7 +990,6 @@ settings' = unsafePerformIO $ do
loggerConfig loggerConfig
brickMain :: AppState brickMain :: AppState
-> IO () -> IO ()
brickMain s = do brickMain s = do
@ -687,12 +998,13 @@ brickMain s = do
eAppData <- getAppData (Just $ ghcupInfo s) eAppData <- getAppData (Just $ ghcupInfo s)
case eAppData of case eAppData of
Right ad -> Right ad ->
defaultMain Brick.defaultMain
(app (defaultAttributes (noColor $ settings s)) (dimAttributes (noColor $ settings s))) (app (defaultAttributes (noColor $ settings s)) (dimAttributes (noColor $ settings s)))
(BrickState ad (BrickState ad
defaultAppSettings defaultAppSettings
(constructList ad defaultAppSettings Nothing) (constructList ad defaultAppSettings Nothing)
(keyBindings (s :: AppState)) (keyBindings (s :: AppState))
Navigation
) )
$> () $> ()
@ -702,7 +1014,7 @@ brickMain s = do
defaultAppSettings :: BrickSettings defaultAppSettings :: BrickSettings
defaultAppSettings = BrickSettings { showAllVersions = False } defaultAppSettings = BrickSettings { _showAllVersions = False}
getGHCupInfo :: IO (Either String GHCupInfo) getGHCupInfo :: IO (Either String GHCupInfo)

View File

@ -327,6 +327,8 @@ executable ghcup
, brick ^>=2.1 , brick ^>=2.1
, transformers ^>=0.5 , transformers ^>=0.5
, vty ^>=6.0 , vty ^>=6.0
, unix ^>=2.7
, optics ^>=0.4
if os(windows) if os(windows)
cpp-options: -DIS_WINDOWS cpp-options: -DIS_WINDOWS