diff --git a/lib-opt/GHCup/OptParse/Compile.hs b/lib-opt/GHCup/OptParse/Compile.hs index 1f857f63..8dd6fa9b 100644 --- a/lib-opt/GHCup/OptParse/Compile.hs +++ b/lib-opt/GHCup/OptParse/Compile.hs @@ -218,7 +218,7 @@ ghcCompileOpts = ( long "hadrian-ghc" <> metavar "HADRIAN_GHC" <> help - "The GHC version (or full path) to GHC that will be used to compile hadrian (must be installed)" + "The GHC version (or full path) that will be used to compile hadrian (must be installed)" <> (completer $ versionCompleter [] GHC) )) <*> optional diff --git a/lib-tui/GHCup/Brick/Actions.hs b/lib-tui/GHCup/Brick/Actions.hs index 24381ad5..020ce0bf 100644 --- a/lib-tui/GHCup/Brick/Actions.hs +++ b/lib-tui/GHCup/Brick/Actions.hs @@ -30,6 +30,7 @@ import qualified GHCup.Brick.Widgets.Menus.Context as ContextMenu import GHCup.Brick.Widgets.Navigation (BrickInternalState) import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall import qualified GHCup.Brick.Widgets.Menus.CompileGHC as CompileGHC +import GHCup.Brick.Widgets.Menu (MenuKeyBindings(..)) import qualified Brick import qualified Brick.Widgets.List as L @@ -714,14 +715,14 @@ keyHandlers KeyBindings {..} = where createMenuforTool = do e <- use (appState % to sectionListSelectedElement) - let exitKey = KeyCombination (Vty.KChar 'c') [Vty.MCtrl] case e of Nothing -> pure () Just (_, r) -> do -- Create new ContextMenu, but maintain the state of Install/Compile -- menus. This is especially useful in case the user made a typo and -- would like to retry the action. - contextMenu .= ContextMenu.create r exitKey + contextMenu .= ContextMenu.create r + (MenuKeyBindings { mKbUp = bUp, mKbDown = bDown, mKbQuit = bQuit}) -- Set mode to context mode .= ContextPanel pure () diff --git a/lib-tui/GHCup/Brick/App.hs b/lib-tui/GHCup/Brick/App.hs index 2b1d5ddd..fac6c1b9 100644 --- a/lib-tui/GHCup/Brick/App.hs +++ b/lib-tui/GHCup/Brick/App.hs @@ -35,7 +35,8 @@ import qualified GHCup.Brick.Widgets.Tutorial as Tutorial import qualified GHCup.Brick.Widgets.Menu as Menu import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall -import GHCup.Types (AppState (AppState, keyBindings), KeyCombination (KeyCombination)) +import GHCup.List (ListResult) +import GHCup.Types (AppState (AppState, keyBindings), KeyCombination (KeyCombination), KeyBindings (..)) import qualified Brick.Focus as F import Brick ( @@ -48,7 +49,7 @@ import Brick ( ) import qualified Brick import Control.Monad.Reader ( - MonadIO (liftIO), + MonadIO (liftIO), ReaderT, void, ) import Data.IORef (readIORef) @@ -59,6 +60,7 @@ import qualified Graphics.Vty as Vty import qualified Data.Text as T +import Optics (Lens') import Optics.Getter (to) import Optics.Operators ((^.)) import Optics.Optic ((%)) @@ -91,26 +93,31 @@ drawUI dimAttrs st = navg = Navigation.draw dimAttrs (st ^. appState) <=> footer in case st ^. mode of Navigation -> [navg] - Tutorial -> [Tutorial.draw, navg] + Tutorial -> [Tutorial.draw (bQuit $ st ^. appKeys), navg] KeyInfo -> [KeyInfo.draw (st ^. appKeys), navg] ContextPanel -> [ContextMenu.draw (st ^. contextMenu), navg] - AdvanceInstallPanel -> [AdvanceInstall.draw (st ^. advanceInstallMenu), navg] - CompileGHCPanel -> [CompileGHC.draw (st ^. compileGHCMenu), navg] - CompileHLSPanel -> [CompileHLS.draw (st ^. compileHLSMenu), navg] + AdvanceInstallPanel -> AdvanceInstall.draw (st ^. advanceInstallMenu) ++ [navg] + CompileGHCPanel -> CompileGHC.draw (st ^. compileGHCMenu) ++ [navg] + CompileHLSPanel -> CompileHLS.draw (st ^. compileHLSMenu) ++ [navg] -- | On q, go back to navigation. -- On Enter, to go to tutorial keyInfoHandler :: BrickEvent Name e -> EventM Name BrickState () -keyInfoHandler ev = case ev of - VtyEvent (Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl]) -> mode .= Navigation - VtyEvent (Vty.EvKey Vty.KEnter _ ) -> mode .= Tutorial - _ -> pure () +keyInfoHandler ev = do + AppState { keyBindings = kb } <- liftIO $ readIORef Actions.settings' + case ev of + VtyEvent (Vty.EvKey Vty.KEnter _ ) -> mode .= Tutorial + VtyEvent (Vty.EvKey key mods) + | bQuit kb == KeyCombination key mods -> mode .= Navigation + _ -> pure () -- | On q, go back to navigation. Else, do nothing tutorialHandler :: BrickEvent Name e -> EventM Name BrickState () -tutorialHandler ev = +tutorialHandler ev = do + AppState { keyBindings = kb } <- liftIO $ readIORef Actions.settings' case ev of - VtyEvent (Vty.EvKey (Vty.KChar 'c') [Vty.MCtrl]) -> mode .= Navigation + VtyEvent (Vty.EvKey key mods) + | bQuit kb == KeyCombination key mods -> mode .= Navigation _ -> pure () -- | Tab/Arrows to navigate. @@ -128,7 +135,7 @@ contextMenuHandler :: BrickEvent Name e -> EventM Name BrickState () contextMenuHandler ev = do ctx <- use contextMenu let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent - (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL + (KeyCombination exitKey mods) = ctx ^. Menu.menuKeyBindingsL % Menu.mKbQuitL case (ev, focusedElement) of (_ , Nothing) -> pure () (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= Navigation @@ -138,47 +145,35 @@ contextMenuHandler ev = do _ -> Common.zoom contextMenu $ ContextMenu.handler ev -- advanceInstallHandler :: BrickEvent Name e -> EventM Name BrickState () -advanceInstallHandler ev = do - ctx <- use advanceInstallMenu - let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent - (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL - case (ev, focusedElement) of - (_ , Nothing) -> pure () - (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel - (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do - let iopts = ctx ^. Menu.menuStateL - when (Menu.isValidMenu ctx) $ - Actions.withIOAction $ Actions.installWithOptions iopts - _ -> Common.zoom advanceInstallMenu $ AdvanceInstall.handler ev +advanceInstallHandler = menuWithOverlayHandler advanceInstallMenu Actions.installWithOptions AdvanceInstall.handler compileGHCHandler :: BrickEvent Name e -> EventM Name BrickState () -compileGHCHandler ev = do - ctx <- use compileGHCMenu - let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent - (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL - case (ev, focusedElement) of - (_ , Nothing) -> pure () - (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel - (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do - let iopts = ctx ^. Menu.menuStateL - when (Menu.isValidMenu ctx) - (Actions.withIOAction $ Actions.compileGHC iopts) - _ -> Common.zoom compileGHCMenu $ CompileGHC.handler ev - +compileGHCHandler = menuWithOverlayHandler compileGHCMenu Actions.compileGHC CompileGHC.handler compileHLSHandler :: BrickEvent Name e -> EventM Name BrickState () -compileHLSHandler ev = do - ctx <- use compileHLSMenu +compileHLSHandler = menuWithOverlayHandler compileHLSMenu Actions.compileHLS CompileHLS.handler + +-- | Passes all events to innerHandler if an overlay is opened +-- else handles the exitKey and Enter key for the Menu's "OkButton" +menuWithOverlayHandler :: Lens' BrickState (Menu.Menu t Name) + -> (t -> ((Int, ListResult) -> ReaderT AppState IO (Either String a))) + -> (BrickEvent Name e -> EventM Name (Menu.Menu t Name) ()) + -> BrickEvent Name e + -> EventM Name BrickState () +menuWithOverlayHandler accessor action innerHandler ev = do + ctx <- use accessor let focusedElement = ctx ^. Menu.menuFocusRingL % to F.focusGetCurrent - (KeyCombination exitKey mods) = ctx ^. Menu.menuExitKeyL - case (ev, focusedElement) of - (_ , Nothing) -> pure () - (VtyEvent (Vty.EvKey k m), Just n) | k == exitKey && m == mods -> mode .= ContextPanel - (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton)) -> do + focusedField = (\n -> find (\x -> Brick.getName x == n) $ ctx ^. Menu.menuFieldsL) =<< focusedElement + (KeyCombination exitKey mods) = ctx ^. Menu.menuKeyBindingsL % Menu.mKbQuitL + case (ev, focusedElement, Menu.drawFieldOverlay =<< focusedField) of + (_ , Nothing, _) -> pure () + (_ , _, Just _) -> Common.zoom accessor $ innerHandler ev + (VtyEvent (Vty.EvKey k m), Just n, _) | k == exitKey && m == mods -> mode .= ContextPanel + (VtyEvent (Vty.EvKey Vty.KEnter []), Just (MenuElement Common.OkButton), _) -> do let iopts = ctx ^. Menu.menuStateL when (Menu.isValidMenu ctx) - (Actions.withIOAction $ Actions.compileHLS iopts) - _ -> Common.zoom compileHLSMenu $ CompileHLS.handler ev + (Actions.withIOAction $ action iopts) + _ -> Common.zoom accessor $ innerHandler ev eventHandler :: BrickEvent Name e -> EventM Name BrickState () eventHandler ev = do diff --git a/lib-tui/GHCup/Brick/Attributes.hs b/lib-tui/GHCup/Brick/Attributes.hs index 8226f46a..e6ed39c9 100644 --- a/lib-tui/GHCup/Brick/Attributes.hs +++ b/lib-tui/GHCup/Brick/Attributes.hs @@ -40,7 +40,7 @@ defaultAttributes no_color = Brick.attrMap , (dayAttr , Vty.defAttr `withForeColor` Vty.brightCyan) , (helpAttr , Vty.defAttr `withStyle` Vty.italic) , (hoorayAttr , Vty.defAttr `withForeColor` Vty.brightWhite) - , (helpMsgAttr , Vty.defAttr `withForeColor` Vty.brightBlack) + , (helpMsgAttr , Vty.defAttr `withForeColor` Vty.yellow) , (errMsgAttr , Vty.defAttr `withForeColor` Vty.red) ] where diff --git a/lib-tui/GHCup/Brick/Common.hs b/lib-tui/GHCup/Brick/Common.hs index 684220f3..db3b54ff 100644 --- a/lib-tui/GHCup/Brick/Common.hs +++ b/lib-tui/GHCup/Brick/Common.hs @@ -48,6 +48,7 @@ module GHCup.Brick.Common ( , BuildFlavourEditBox, BuildSystemEditBox, OkButton, AdvanceInstallButton , CompileGHCButton, CompileHLSButton, CabalProjectEditBox , CabalProjectLocalEditBox, UpdateCabalCheckBox, GitRefEditBox + , BootstrapGhcSelectBox, HadrianGhcSelectBox ) ) where import GHCup.List ( ListResult ) @@ -98,7 +99,7 @@ pattern TargetGhcEditBox = ResourceId 6 pattern BootstrapGhcEditBox :: ResourceId pattern BootstrapGhcEditBox = ResourceId 7 pattern HadrianGhcEditBox :: ResourceId -pattern HadrianGhcEditBox = ResourceId 17 +pattern HadrianGhcEditBox = ResourceId 20 pattern JobsEditBox :: ResourceId pattern JobsEditBox = ResourceId 8 pattern BuildConfigEditBox :: ResourceId @@ -126,6 +127,11 @@ pattern UpdateCabalCheckBox = ResourceId 18 pattern GitRefEditBox :: ResourceId pattern GitRefEditBox = ResourceId 19 +pattern BootstrapGhcSelectBox :: ResourceId +pattern BootstrapGhcSelectBox = ResourceId 21 +pattern HadrianGhcSelectBox :: ResourceId +pattern HadrianGhcSelectBox = ResourceId 22 + -- | Name data type. Uniquely identifies each widget in the TUI. -- some constructors might end up unused, but still is a good practise -- to have all of them defined, just in case diff --git a/lib-tui/GHCup/Brick/Widgets/KeyInfo.hs b/lib-tui/GHCup/Brick/Widgets/KeyInfo.hs index dfa4dd4e..9122cfb9 100644 --- a/lib-tui/GHCup/Brick/Widgets/KeyInfo.hs +++ b/lib-tui/GHCup/Brick/Widgets/KeyInfo.hs @@ -69,4 +69,4 @@ draw KeyBindings {..} = ] ] ] - <=> Brick.hBox [Brick.txt "Press c+ctrl to return to Navigation" <+> Brick.padRight Brick.Max (Brick.txt " ") <+> Brick.txt "Press Enter to go to the Tutorial"] + <=> Brick.hBox [Brick.txt "Press " <+> Common.keyToWidget bQuit <+> Brick.txt " to return to Navigation" <+> Brick.padRight Brick.Max (Brick.txt " ") <+> Brick.txt "Press Enter to go to the Tutorial"] diff --git a/lib-tui/GHCup/Brick/Widgets/Menu.hs b/lib-tui/GHCup/Brick/Widgets/Menu.hs index 5a0d1ba1..289d2c89 100644 --- a/lib-tui/GHCup/Brick/Widgets/Menu.hs +++ b/lib-tui/GHCup/Brick/Widgets/Menu.hs @@ -57,6 +57,8 @@ import Brick (<+>)) import qualified Brick import qualified Brick.Widgets.Border as Border +import qualified Brick.Widgets.Border.Style as Border +import qualified Brick.Widgets.Center as Brick import qualified Brick.Widgets.List as L import qualified Brick.Widgets.Edit as Edit import Brick.Focus (FocusRing) @@ -64,6 +66,7 @@ import qualified Brick.Focus as F import Data.Function ( (&)) import Prelude hiding ( appendFile ) +import Data.Maybe import qualified Data.Text as T @@ -71,11 +74,13 @@ import Optics.TH (makeLensesFor) import qualified Graphics.Vty as Vty import Optics.State.Operators ((%=), (.=)) import Optics.Optic ((%)) -import Optics.State (use) -import GHCup.Types (KeyCombination) -import Optics (Lens', to, lens) +import Optics.State (use, assign) +import GHCup.Types (KeyCombination(..)) +import Optics (Lens', to, lens, _1, over) import Optics.Operators ( (^.), (.~) ) -import Data.Foldable (foldl') +import Data.Foldable (find, foldl') +import Data.List.NonEmpty ( NonEmpty (..) ) +import qualified Data.List.NonEmpty as NE -- | Just some type synonym to make things explicit @@ -111,9 +116,10 @@ data FieldInput a b n = , inputRender :: Bool -> ErrorStatus -> HelpMessage + -> Label -> b -> (Widget n -> Widget n) - -> Widget n -- ^ How to draw the input, with focus a help message and input. + -> (Widget n, Maybe (Widget n)) -- ^ How to draw the input and optionally an overlay, with focus a help message and input. -- A extension function can be applied too , inputHandler :: BrickEvent n () -> EventM n b () -- ^ The handler } @@ -146,6 +152,47 @@ makeLensesFor ] ''MenuField +data SelectState i n = SelectState + { selectStateItems :: (NonEmpty (Int, (i, Bool)), Bool) -- ^ All items along with their selected state + -- And Bool to indicate if editable field is selected + , selectStateEditState :: Maybe (Edit.Editor T.Text n) -- ^ Editable field's editor state + , selectStateFocusRing :: FocusRing Int -- ^ Focus ring using integeral values assigned to each item + , selectStateOverlayOpen :: Bool -- ^ Whether the select menu is open + } + +makeLensesFor + [ ("selectStateItems", "selectStateItemsL") + , ("selectStateEditState", "selectStateEditStateL") + , ("selectStateFocusRing", "selectStateFocusRingL") + , ("selectStateOverlayOpen", "selectStateOverlayOpenL") + ] + ''SelectState + +data EditState n = EditState + { editState :: Edit.Editor T.Text n + , editStateOverlayOpen :: Bool -- ^ Whether the edit menu is open + } + +makeLensesFor + [ ("editState", "editStateL") + , ("editStateOverlayOpen", "editStateOverlayOpenL") + ] + ''EditState + +data MenuKeyBindings = MenuKeyBindings + { mKbUp :: KeyCombination + , mKbDown :: KeyCombination + , mKbQuit :: KeyCombination + } + deriving (Show) + +makeLensesFor + [ ("mKbUp", "mKbUpL") + , ("mKbDown", "mKbDownL") + , ("mKbQuit", "mKbQuitL") + ] + ''MenuKeyBindings + -- | A fancy lens to the help message fieldHelpMsgL :: Lens' (MenuField s n) HelpMessage fieldHelpMsgL = lens g s @@ -155,10 +202,14 @@ fieldHelpMsgL = lens g s -- | How to draw a field given a formater drawField :: Formatter n -> Bool -> MenuField s n -> Widget n drawField amp focus (MenuField { fieldInput = FieldInput {..}, ..}) = - let input = inputRender focus fieldStatus inputHelp inputState (amp focus) - in if focus - then Common.enableScreenReader fieldName $ Brick.visible input - else input + let (input, overlay) = inputRender focus fieldStatus inputHelp fieldLabel inputState (amp focus) + in case (focus, overlay) of + (True, Nothing) -> Common.enableScreenReader fieldName $ Brick.visible input + _ -> input + +drawFieldOverlay :: MenuField s n -> Maybe (Widget n) +drawFieldOverlay (MenuField { fieldInput = FieldInput {..}, ..}) = + snd $ inputRender True fieldStatus inputHelp fieldLabel inputState id instance Brick.Named (MenuField s n) n where getName :: MenuField s n -> n @@ -179,7 +230,7 @@ createCheckBoxInput = FieldInput False Right "" checkBoxRender checkBoxHandler if b then border . Brick.withAttr Attributes.installedAttr $ Brick.str Common.installedSign else border . Brick.withAttr Attributes.notInstalledAttr $ Brick.str Common.notInstalledSign - checkBoxRender focus _ help check f = + checkBoxRender focus _ help _ check f = (, Nothing) $ let core = f $ drawBool check in if focus then core @@ -197,25 +248,48 @@ createCheckBoxField name access = MenuField access createCheckBoxInput "" Valid type EditableField = MenuField -createEditableInput :: (Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> FieldInput a (Edit.Editor T.Text n) n -createEditableInput name validator = FieldInput initEdit validateEditContent "" drawEdit Edit.handleEditorEvent +createEditableInput :: (Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> FieldInput a (EditState n) n +createEditableInput name validator = FieldInput initEdit validateEditContent "" drawEdit handler where - drawEdit focus errMsg help edi amp = - let - borderBox w = amp (Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max w <+> Border.vBorder) - editorRender = Edit.renderEditor (Brick.txt . T.unlines) focus edi - isEditorEmpty = Edit.getEditContents edi == [mempty] - in case errMsg of - Valid | isEditorEmpty -> borderBox $ renderAsHelpMsg help - | otherwise -> borderBox editorRender - Invalid msg - | focus && isEditorEmpty -> borderBox $ renderAsHelpMsg help - | focus -> borderBox editorRender - | otherwise -> borderBox $ renderAsErrMsg msg - validateEditContent = validator . T.init . T.unlines . Edit.getEditContents - initEdit = Edit.editorText name (Just 1) "" - -createEditableField :: (Eq n, Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> Lens' s a -> EditableField s n + drawEdit focus errMsg help label (EditState edi overlayOpen) amp = (field, mOverlay) + where + field = + let + borderBox w = amp (Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max w <+> Border.vBorder) + editorContents = Brick.txt $ T.unlines $ Edit.getEditContents edi + isEditorEmpty = Edit.getEditContents edi == [mempty] + in case errMsg of + Valid | isEditorEmpty -> borderBox $ renderAsHelpMsg help + | otherwise -> borderBox editorContents + Invalid msg + | focus && isEditorEmpty -> borderBox $ renderAsHelpMsg help + | focus -> borderBox editorContents + | otherwise -> borderBox $ renderAsErrMsg msg + mOverlay = if overlayOpen + then Just (overlayLayer ("Edit " <> label) $ overlay) + else Nothing + overlay = Brick.vBox $ + [ Brick.txtWrap help + , Border.border $ Edit.renderEditor (Brick.txt . T.unlines) focus edi + , case errMsg of + Invalid msg -> renderAsErrMsg msg + _ -> Brick.txt " " + , Brick.padRight Brick.Max $ + Brick.txt "Press Enter to go back" + ] + handler ev = do + (EditState edi overlayOpen) <- Brick.get + if overlayOpen + then case ev of + VtyEvent (Vty.EvKey Vty.KEnter []) -> editStateOverlayOpenL .= False + _ -> Common.zoom editStateL $ Edit.handleEditorEvent ev + else case ev of + VtyEvent (Vty.EvKey Vty.KEnter []) -> editStateOverlayOpenL .= True + _ -> pure () + validateEditContent = validator . T.init . T.unlines . Edit.getEditContents . editState + initEdit = EditState (Edit.editorText name (Just 1) "") False + +createEditableField :: (Eq n, Ord n, Show n) => n -> (T.Text -> Either ErrorMessage a) -> Lens' s a -> EditableField s n createEditableField name validator access = MenuField access input "" Valid name where input = createEditableInput name validator @@ -229,12 +303,149 @@ type Button = MenuField createButtonInput :: FieldInput () () n createButtonInput = FieldInput () Right "" drawButton (const $ pure ()) where - drawButton True (Invalid err) _ _ amp = amp . centerV . renderAsErrMsg $ err - drawButton _ _ help _ amp = amp . centerV . renderAsHelpMsg $ help + drawButton True (Invalid err) _ _ _ amp = (amp . renderAsErrMsg $ err, Nothing) + drawButton _ _ help _ _ amp = + let pad = if length (T.lines help) == 1 then Brick.padTop (Brick.Pad 1) else id + in (amp . pad . renderAsHelpMsg $ help, Nothing) createButtonField :: n -> Button s n createButtonField = MenuField emptyLens createButtonInput "" Valid +{- ***************** + Select widget +***************** -} + +type SelectField = MenuField + +createSelectInput :: (Ord n, Show n) + => NonEmpty i + -> (i -> T.Text) + -> (Int -> (NonEmpty (Int, (i, Bool)), Bool) -> ((NonEmpty (Int, (i, Bool))), Bool)) + -> (([i], Maybe T.Text) -> Either ErrorMessage k) + -> n + -> Maybe n + -> MenuKeyBindings + -> FieldInput k (SelectState i n) n +createSelectInput items showItem updateSelection validator viewportFieldName mEditFieldName kb + = FieldInput initState (validator . getSelectedItems) "" selectRender selectHandler + where + totalRows = (if isJust mEditFieldName then (+) 1 else id) $ length items + initState = SelectState + (NE.zip (1 NE.:| [2..]) $ fmap (,False) items, False) + ((\n -> Edit.editorText n (Just 1) "") <$> mEditFieldName) + (F.focusRing [1.. totalRows]) + False + getSelectedItems (SelectState {..}) = + ( fmap (fst . snd) . (filter (snd . snd)) . NE.toList . fst $ selectStateItems + , if snd selectStateItems then (T.init . T.unlines . Edit.getEditContents <$> selectStateEditState) else Nothing) + + border w = Brick.txt "[" <+> (Brick.padRight (Brick.Pad 1) $ Brick.padLeft (Brick.Pad 1) w) <+> Brick.txt "]" + selectRender focus errMsg help label s amp = (field, mOverlay) + where + field = + let mContents = case getSelectedItems s of + ([], Nothing) -> Nothing + (xs, mTxt) -> Just $ fmap (Brick.padRight (Brick.Pad 1) . Brick.txt . showItem) xs + ++ (case mTxt of Just t -> [Brick.txt t]; Nothing -> []) + in amp $ case (errMsg, mContents) of + (Valid, Nothing) -> (Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help) + (Valid, Just contents) -> border $ Brick.hBox contents + (Invalid msg, Nothing) + | focus -> Brick.padLeft (Brick.Pad 1) . renderAsHelpMsg $ help + | otherwise -> Brick.padLeft (Brick.Pad 1) $ renderAsErrMsg msg + (Invalid msg, Just contents) + | focus -> border $ Brick.hBox contents + | otherwise -> Brick.padLeft (Brick.Pad 1) $ renderAsErrMsg msg + + mOverlay = if selectStateOverlayOpen s + then Just (overlayLayer ("Select " <> label) $ overlay s errMsg help) + else Nothing + overlay (SelectState {..}) errMsg help = Brick.vBox $ + [ if txtFieldFocused + then Brick.txtWrap "Press Enter to finish editing and select custom value. Press Up/Down keys to navigate" + else Brick.txt "Press " + <+> Common.keyToWidget (kb ^. mKbQuitL) + <+> Brick.txt " to go back, Press Enter to select" + , case errMsg of Invalid msg -> renderAsErrMsg msg; _ -> Brick.emptyWidget + , Brick.vLimit (totalRows) $ Brick.withVScrollBars Brick.OnRight + $ Brick.viewport viewportFieldName Brick.Vertical + $ Brick.vBox $ mEditableField ++ (NE.toList $ fmap (mkSelectRow focused) (fst selectStateItems)) + ] + where focused = fromMaybe 1 $ F.focusGetCurrent selectStateFocusRing + txtFieldFocused = focused == totalRows + mEditableField = case selectStateEditState of + Just edi -> [ mkEditTextRow txtFieldFocused edi (snd selectStateItems) help ] + Nothing -> [] + + mkSelectRow focused (ix, (item, selected)) = (if focused == ix then Brick.visible else id) $ + Brick.txt "[" <+> (Brick.padRight (Brick.Pad 1) $ Brick.padLeft (Brick.Pad 1) m) <+> Brick.txt "] " + <+> (renderAslabel (showItem item) (focused == ix)) + where m = if selected then Brick.txt "*" else Brick.txt " " + + mkEditTextRow focused edi selected help = (if focused then Brick.visible else id) $ + Brick.txt "[" <+> (Brick.padRight (Brick.Pad 1) $ Brick.padLeft (Brick.Pad 1) m) <+> Brick.txt "] " + <+> if not focused && Edit.getEditContents edi == [mempty] + then Brick.txt "(Specify custom text value)" + else Brick.vLimit 1 $ Border.vBorder <+> Brick.padRight Brick.Max (Edit.renderEditor (Brick.txt . T.unlines) focused edi) <+> Border.vBorder + where m = if selected then Brick.txt "*" else Brick.txt " " + + selectHandler ev = do + s <- Brick.get + if selectStateOverlayOpen s + then do + focused <- use (selectStateFocusRingL % to F.focusGetCurrent) + mEditState <- use selectStateEditStateL + case (focused, mEditState) of + (Just ix, Just edi) + | ix == totalRows -> case ev of + VtyEvent (Vty.EvKey Vty.KEnter []) -> do + selectStateItemsL %= updateSelection ix + selectStateFocusRingL %= F.focusNext + VtyEvent (Vty.EvKey Vty.KDown []) -> selectStateFocusRingL %= F.focusNext + VtyEvent (Vty.EvKey Vty.KUp []) -> selectStateFocusRingL %= F.focusPrev + _ -> do + newEdi <- Brick.nestEventM' edi $ Edit.handleEditorEvent ev + assign selectStateEditStateL (Just newEdi) + selectStateItemsL %= updateSelection ix + _ -> case ev of + VtyEvent (Vty.EvKey k m) + | KeyCombination k m == kb ^. mKbQuitL -> selectStateOverlayOpenL .= False + | KeyCombination k m == kb ^. mKbUpL -> selectStateFocusRingL %= F.focusPrev + | KeyCombination k m == kb ^. mKbDownL -> selectStateFocusRingL %= F.focusNext + VtyEvent (Vty.EvKey Vty.KEnter []) -> do + selectStateItemsL %= updateSelection (fromMaybe 1 focused) + _ -> pure () + else case ev of + VtyEvent (Vty.EvKey Vty.KEnter []) -> selectStateOverlayOpenL .= True + _ -> pure () + +-- | Select Field with only single selection possible, aka radio button +createSelectField :: (Ord n, Show n) => n -> Lens' s (Maybe i) -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n +createSelectField name access items showItem keyBindings = MenuField access (createSelectInput items showItem singleSelect getSelection name Nothing keyBindings) "" Valid name + where + singleSelect :: Int -> (NonEmpty (Int, (i, Bool)), a) -> (NonEmpty (Int, (i, Bool)), a) + singleSelect ix = over _1 $ fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, True)) else (ix', (i, False))) + + getSelection = Right . fmap NE.head . NE.nonEmpty . fst + +-- | Select Field with multiple selections possible +createMultiSelectField :: (Ord n, Show n) => n -> Lens' s [i] -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n +createMultiSelectField name access items showItem keyBindings = MenuField access (createSelectInput items showItem multiSelect (Right . fst) name Nothing keyBindings) "" Valid name + where + multiSelect :: Int -> (NonEmpty (Int, (i, Bool)), a) -> (NonEmpty (Int, (i, Bool)), a) + multiSelect ix = over _1 $ fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, not b)) else (ix', (i, b))) + +-- | Select Field with only single selection possible, along with an editable field +createSelectFieldWithEditable :: (Ord n, Show n) => n -> n -> Lens' s (Either a i) -> (T.Text -> Either ErrorMessage a) -> NonEmpty i -> (i -> T.Text) -> MenuKeyBindings -> SelectField s n +createSelectFieldWithEditable name editFieldName access validator items showItem keyBindings = MenuField access (createSelectInput items showItem singleSelect getSelection name (Just editFieldName) keyBindings) "" Valid name + where + singleSelect :: Int -> (NonEmpty (Int, (i, Bool)), Bool) -> (NonEmpty (Int, (i, Bool)), Bool) + singleSelect ix (ne, a) = (fmap (\(ix', (i, b)) -> if ix' == ix then (ix', (i, True)) else (ix', (i, False))) ne, ix == length ne + 1) + + getSelection (_, Just txt) = either Left (Right . Left) $ validator txt + getSelection (ls, _) = maybe (either Left (Right . Left) $ validator "") (Right . Right . NE.head) $ NE.nonEmpty ls + + {- ***************** Utilities ***************** -} @@ -264,10 +475,6 @@ leftify i = Brick.hLimit i . Brick.padRight Brick.Max rightify :: Int -> Brick.Widget n -> Brick.Widget n rightify i = Brick.hLimit i . Brick.padLeft Brick.Max --- | center a line in three rows. -centerV :: Widget n -> Widget n -centerV = Brick.padTopBottom 1 - -- | render some Text using helpMsgAttr renderAsHelpMsg :: T.Text -> Widget n renderAsHelpMsg = Brick.withAttr Attributes.helpMsgAttr . Brick.txt @@ -276,6 +483,15 @@ renderAsHelpMsg = Brick.withAttr Attributes.helpMsgAttr . Brick.txt renderAsErrMsg :: T.Text -> Widget n renderAsErrMsg = Brick.withAttr Attributes.errMsgAttr . Brick.txt +-- | Used to create a layer on top of menu +overlayLayer :: T.Text -> Brick.Widget n -> Brick.Widget n +overlayLayer layer_name = + Brick.centerLayer + . Brick.hLimitPercent 50 + . Brick.vLimitPercent 65 + . Brick.withBorderStyle Border.unicode + . Border.borderWithLabel (Brick.txt layer_name) + {- ***************** Menu widget ***************** -} @@ -289,14 +505,16 @@ data Menu s n , menuValidator :: s -> Maybe ErrorMessage -- ^ A validator function , menuButtons :: [Button s n] -- ^ The buttons. Commonly, the handlers for buttons are defined outside the menu handler. , menuFocusRing :: FocusRing n -- ^ The focus ring with the resource name for each entry and each button, in the order you want to loop them. - , menuExitKey :: KeyCombination -- ^ The key to exit the Menu + , menuKeyBindings :: MenuKeyBindings -- ^ KeyBindings for navigation , menuName :: n -- ^ The resource Name. + , menuTitle :: T.Text -- ^ Menu title. } makeLensesFor [ ("menuFields", "menuFieldsL"), ("menuState", "menuStateL"), ("menuValidator", "menuValidatorL") , ("menuButtons", "menuButtonsL"), ("menuFocusRing", "menuFocusRingL") - , ("menuExitKey", "menuExitKeyL"), ("menuName", "menuNameL") + , ("menuKeyBindings", "menuKeyBindingsL"), ("menuName", "menuNameL") + , ("menuTitle", "menuTitleL") ] ''Menu @@ -304,22 +522,18 @@ isValidMenu :: Menu s n -> Bool isValidMenu m = (all isValidField $ menuFields m) && (case (menuValidator m) (menuState m) of { Nothing -> True; _ -> False }) -createMenu :: n -> s -> (s -> Maybe ErrorMessage) - -> KeyCombination -> [Button s n] -> [MenuField s n] -> Menu s n -createMenu n initial validator exitK buttons fields = Menu fields initial validator buttons ring exitK n +createMenu :: n -> s -> T.Text -> (s -> Maybe ErrorMessage) + -> MenuKeyBindings -> [Button s n] -> [MenuField s n] -> Menu s n +createMenu n initial title validator keys buttons fields = Menu fields initial validator buttons ring keys n title where ring = F.focusRing $ [field & fieldName | field <- fields] ++ [button & fieldName | button <- buttons] handlerMenu :: forall n e s. Eq n => BrickEvent n e -> EventM n (Menu s n) () -handlerMenu ev = - case ev of - VtyEvent (Vty.EvKey (Vty.KChar '\t') []) -> menuFocusRingL %= F.focusNext - VtyEvent (Vty.EvKey Vty.KBackTab []) -> menuFocusRingL %= F.focusPrev - VtyEvent (Vty.EvKey Vty.KDown []) -> menuFocusRingL %= F.focusNext - VtyEvent (Vty.EvKey Vty.KUp []) -> menuFocusRingL %= F.focusPrev - VtyEvent e -> do - focused <- use $ menuFocusRingL % to F.focusGetCurrent - fields <- use menuFieldsL - case focused of +handlerMenu ev = do + fields <- use menuFieldsL + kb <- use menuKeyBindingsL + focused <- use $ menuFocusRingL % to F.focusGetCurrent + let focusedField = (\n -> find (\x -> Brick.getName x == n) fields) =<< focused + propagateEvent e = case focused of Nothing -> pure () Just n -> do updated_fields <- updateFields n (VtyEvent e) fields @@ -331,7 +545,16 @@ handlerMenu ev = Just err -> menuButtonsL %= fmap (fieldStatusL .~ Invalid err) else menuButtonsL %= fmap (fieldStatusL .~ Invalid "Some fields are invalid") menuFieldsL .= updated_fields - _ -> pure () + case (drawFieldOverlay =<< focusedField) of + Just _ -> case ev of + VtyEvent e -> propagateEvent e + _ -> pure () + Nothing -> case ev of + VtyEvent (Vty.EvKey k m) + | KeyCombination k m == kb ^. mKbUpL -> menuFocusRingL %= F.focusPrev + | KeyCombination k m == kb ^. mKbDownL -> menuFocusRingL %= F.focusNext + VtyEvent e -> propagateEvent e + _ -> pure () where -- runs the Event with the inner handler of MenuField. updateFields :: n -> BrickEvent n () -> [MenuField s n] -> EventM n (Menu s n) [MenuField s n] @@ -346,9 +569,12 @@ handlerMenu ev = else pure x -drawMenu :: (Eq n, Ord n, Show n, Brick.Named (MenuField s n) n) => Menu s n -> Widget n +drawMenu :: (Eq n, Ord n, Show n, Brick.Named (MenuField s n) n) => Menu s n -> [Widget n] drawMenu menu = - Brick.vBox + overlays ++ + [Common.frontwardLayer (menu ^. menuTitleL) mainLayer] + where + mainLayer = Brick.vBox [ Brick.vBox buttonWidgets , Common.separator , Brick.vLimit (length fieldLabels) $ Brick.withVScrollBars Brick.OnRight @@ -357,10 +583,9 @@ drawMenu menu = , Brick.txt " " , Brick.padRight Brick.Max $ Brick.txt "Press " - <+> Common.keyToWidget (menu ^. menuExitKeyL) - <+> Brick.txt " to go back" + <+> Common.keyToWidget (menu ^. menuKeyBindingsL % mKbQuitL) + <+> Brick.txt " to go back, Press Enter to edit the highlighted field" ] - where fieldLabels = [field & fieldLabel | field <- menu ^. menuFieldsL] buttonLabels = [button & fieldLabel | button <- menu ^. menuButtonsL] allLabels = fieldLabels ++ buttonLabels @@ -379,3 +604,5 @@ drawMenu menu = in fmap (\f b -> ((leftify (maxWidth + 2) . Border.border $ f b) <+>) ) buttonAsWidgets drawButtons = fmap drawField buttonAmplifiers buttonWidgets = zipWith (F.withFocusRing (menu ^. menuFocusRingL)) drawButtons (menu ^. menuButtonsL) + + overlays = catMaybes $ fmap drawFieldOverlay (menu ^. menuFieldsL) diff --git a/lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs b/lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs index c9fc8635..19845678 100644 --- a/lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs +++ b/lib-tui/GHCup/Brick/Widgets/Menus/AdvanceInstall.hs @@ -27,7 +27,7 @@ module GHCup.Brick.Widgets.Menus.AdvanceInstall ( addConfArgsL, ) where -import GHCup.Brick.Widgets.Menu (Menu) +import GHCup.Brick.Widgets.Menu (Menu, MenuKeyBindings) import qualified GHCup.Brick.Widgets.Menu as Menu import GHCup.Brick.Common(Name(..)) import Brick @@ -37,7 +37,6 @@ import Brick import Prelude hiding ( appendFile ) import Optics.TH (makeLensesFor) import qualified GHCup.Brick.Common as Common -import GHCup.Types (KeyCombination) import URI.ByteString (URI) import qualified Data.Text as T import Data.Bifunctor (Bifunctor(..)) @@ -65,8 +64,8 @@ makeLensesFor [ type AdvanceInstallMenu = Menu InstallOptions Name -create :: KeyCombination -> AdvanceInstallMenu -create k = Menu.createMenu AdvanceInstallBox initialState validator k [ok] fields +create :: MenuKeyBindings -> AdvanceInstallMenu +create k = Menu.createMenu AdvanceInstallBox initialState "Advance Install" validator k [ok] fields where initialState = InstallOptions Nothing False Nothing False [] validator InstallOptions {..} = case (instSet, isolateDir) of @@ -114,5 +113,5 @@ handler :: BrickEvent Name e -> EventM Name AdvanceInstallMenu () handler = Menu.handlerMenu -draw :: AdvanceInstallMenu -> Widget Name -draw = Common.frontwardLayer "Advance Install" . Menu.drawMenu +draw :: AdvanceInstallMenu -> [Widget Name] +draw = Menu.drawMenu diff --git a/lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs b/lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs index 2fb8d729..705110f6 100644 --- a/lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs +++ b/lib-tui/GHCup/Brick/Widgets/Menus/CompileGHC.hs @@ -35,7 +35,7 @@ module GHCup.Brick.Widgets.Menus.CompileGHC ( gitRef, ) where -import GHCup.Brick.Widgets.Menu (Menu) +import GHCup.Brick.Widgets.Menu (Menu, MenuKeyBindings) import qualified GHCup.Brick.Widgets.Menu as Menu import GHCup.Brick.Common(Name(..)) import Brick @@ -46,18 +46,22 @@ import Prelude hiding ( appendFile ) import Optics.TH (makeLenses) import qualified GHCup.Brick.Common as Common import GHCup.Types - ( KeyCombination, BuildSystem(..), VersionPattern ) + ( BuildSystem(..), VersionPattern ) import URI.ByteString (URI) +import Control.Monad (join) import qualified Data.Text as T import Data.Bifunctor (Bifunctor(..)) import Data.Function ((&)) -import Optics ((.~)) +import Optics ((.~), iso, (%)) import Data.Char (isSpace) +import Data.List.NonEmpty ( NonEmpty (..) ) +import qualified Data.List.NonEmpty as NE import Data.Versions (Version, version) import System.FilePath (isPathSeparator) import Control.Applicative (Alternative((<|>))) import Text.Read (readEither) import qualified GHCup.Utils.Parsers as Utils +import Text.PrettyPrint.HughesPJClass ( prettyShow ) data CompileGHCOptions = CompileGHCOptions { _bootstrapGhc :: Either Version FilePath @@ -79,8 +83,8 @@ makeLenses ''CompileGHCOptions type CompileGHCMenu = Menu CompileGHCOptions Name -create :: KeyCombination -> CompileGHCMenu -create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields +create :: MenuKeyBindings -> [Version] -> CompileGHCMenu +create k availableGHCs = Menu.createMenu CompileGHCBox initialState "Compile GHC" validator k buttons fields where initialState = CompileGHCOptions @@ -115,12 +119,12 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields readPath = do mfilepath <- filepathV i case mfilepath of - Nothing -> Left "Invalid Empty value" + Nothing -> Left "Invalid path" Just f -> Right (Right f) in if T.any isPathSeparator i then readPath else readVersion - False -> Left "Invalid Empty value" + False -> Left "No version selected / no path specified" hadrianstrapV :: T.Text -> Either Menu.ErrorMessage (Maybe (Either Version FilePath)) hadrianstrapV i' = @@ -153,23 +157,40 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields additionalValidator :: T.Text -> Either Menu.ErrorMessage [T.Text] additionalValidator = Right . T.split isSpace - systemV :: T.Text -> Either Menu.ErrorMessage (Maybe BuildSystem) - systemV = whenEmpty Nothing readSys - where - readSys i - | T.toLower i == "hadrian" = Right $ Just Hadrian - | T.toLower i == "make" = Right $ Just Make - | otherwise = Left "Not a valid Build System" - - fields = - [ Menu.createEditableField (Common.MenuElement Common.BootstrapGhcEditBox) bootstrapV bootstrapGhc - & Menu.fieldLabelL .~ "bootstrap-ghc" - & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to bootstrap with (must be installed)" - & Menu.fieldStatusL .~ Menu.Invalid "Invalid Empty value" - , Menu.createEditableField (Common.MenuElement Common.HadrianGhcEditBox) hadrianstrapV hadrianGhc - & Menu.fieldLabelL .~ "hadrian-ghc" - & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to GHC that will be used to compile hadrian (must be installed)" - , Menu.createEditableField (Common.MenuElement Common.JobsEditBox) jobsV jobs + showMaybeBuildSystem :: Maybe BuildSystem -> T.Text + showMaybeBuildSystem = \case + Nothing -> "Auto select (prefer hadrian if available, and build config is not specified)" + Just Hadrian -> "hadrian" + Just Make -> "make" + + bootstrapGHCFields = case NE.nonEmpty availableGHCs of + Just ne -> + let bootstrapGhc' = bootstrapGhc % (iso (either (Left . Left) (Left . Right)) (either id Left)) + in [ Menu.createSelectFieldWithEditable (Common.MenuElement Common.BootstrapGhcSelectBox) (Common.MenuElement Common.BootstrapGhcEditBox) bootstrapGhc' bootstrapV ne (T.pack . prettyShow) k + & Menu.fieldLabelL .~ "bootstrap-ghc" + & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to bootstrap with (must be installed)" + & Menu.fieldStatusL .~ Menu.Invalid "No version selected / no path specified" + ] + _ -> [ Menu.createEditableField (Common.MenuElement Common.BootstrapGhcEditBox) bootstrapV bootstrapGhc + & Menu.fieldLabelL .~ "bootstrap-ghc" + & Menu.fieldHelpMsgL .~ "The GHC version (or full path) to bootstrap with (must be installed)" + & Menu.fieldStatusL .~ Menu.Invalid "Invalid empty value" + ] + + hadrianGHCFields = case NE.nonEmpty availableGHCs of + Just ne -> + let hadrianGhc' = hadrianGhc % (iso Left (either id (Just . Left))) + in [ Menu.createSelectFieldWithEditable (Common.MenuElement Common.HadrianGhcSelectBox) (Common.MenuElement Common.HadrianGhcEditBox) hadrianGhc' hadrianstrapV ne (T.pack . prettyShow) k + & Menu.fieldLabelL .~ "hadrian-ghc" + & Menu.fieldHelpMsgL .~ "The GHC version (or full path) that will be used to compile hadrian (must be installed)" + ] + _ -> [ Menu.createEditableField (Common.MenuElement Common.HadrianGhcEditBox) hadrianstrapV hadrianGhc + & Menu.fieldLabelL .~ "hadrian-ghc" + & Menu.fieldHelpMsgL .~ "The GHC version (or full path) that will be used to compile hadrian (must be installed)" + ] + + fields = bootstrapGHCFields ++ hadrianGHCFields ++ + [ Menu.createEditableField (Common.MenuElement Common.JobsEditBox) jobsV jobs & Menu.fieldLabelL .~ "jobs" & Menu.fieldHelpMsgL .~ "How many jobs to use for make" , Menu.createCheckBoxField (Common.MenuElement Common.SetCheckBox) setCompile @@ -190,12 +211,12 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields , Menu.createEditableField (Common.MenuElement Common.CrossTargetEditBox) (Right . Just) crossTarget & Menu.fieldLabelL .~ "cross target" & Menu.fieldHelpMsgL .~ "Build cross-compiler for this platform" - , Menu.createEditableField (Common.MenuElement Common.BuildSystemEditBox) systemV buildSystem + , Menu.createSelectField (Common.MenuElement Common.BuildSystemEditBox) (buildSystem % (iso Just join)) (Nothing :| [Just Hadrian, Just Make]) showMaybeBuildSystem k & Menu.fieldLabelL .~ "build system" - & Menu.fieldHelpMsgL .~ "either 'make' or 'hadrian'" + & Menu.fieldHelpMsgL .~ "Select the build system" , Menu.createEditableField (Common.MenuElement Common.OvewrwiteVerEditBox) versionV overwriteVer & Menu.fieldLabelL .~ "overwrite-version" - & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one" + & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one. Allows to specify patterns: %v (version), %b (branch name), %h (short commit hash), %H (long commit hash), %g ('git describe' output)" , Menu.createEditableField (Common.MenuElement Common.IsolateEditBox) filepathV isolateDir & Menu.fieldLabelL .~ "isolated" & Menu.fieldHelpMsgL .~ "install in an isolated absolute directory instead of the default one" @@ -207,7 +228,7 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields buttons = [ Menu.createButtonField (Common.MenuElement Common.OkButton) & Menu.fieldLabelL .~ "Compile" - & Menu.fieldHelpMsgL .~ "Compile GHC from source with options below" + & Menu.fieldHelpMsgL .~ "Compile GHC from source with options below\nRequired fields: bootstrap-ghc" & Menu.fieldStatusL .~ Menu.Invalid "bootstrap GHC is mandatory" ] @@ -215,5 +236,5 @@ handler :: BrickEvent Name e -> EventM Name CompileGHCMenu () handler = Menu.handlerMenu -draw :: CompileGHCMenu -> Widget Name -draw = Common.frontwardLayer "Compile GHC" . Menu.drawMenu +draw :: CompileGHCMenu -> [Widget Name] +draw = Menu.drawMenu diff --git a/lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs b/lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs index 08dae3c5..97b8711b 100644 --- a/lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs +++ b/lib-tui/GHCup/Brick/Widgets/Menus/CompileHLS.hs @@ -34,7 +34,7 @@ module GHCup.Brick.Widgets.Menus.CompileHLS ( ) where -import GHCup.Brick.Widgets.Menu (Menu) +import GHCup.Brick.Widgets.Menu (Menu, MenuKeyBindings) import qualified GHCup.Brick.Widgets.Menu as Menu import GHCup.Brick.Common(Name(..)) import Brick @@ -44,16 +44,19 @@ import Brick import Prelude hiding ( appendFile ) import Optics.TH (makeLenses) import qualified GHCup.Brick.Common as Common -import GHCup.Types (KeyCombination, VersionPattern, ToolVersion) +import GHCup.Types (VersionPattern, ToolVersion(..)) import URI.ByteString (URI) import qualified Data.Text as T import Data.Bifunctor (Bifunctor(..)) +import qualified Data.List.NonEmpty as NE import Data.Function ((&)) import Optics ((.~)) import Data.Char (isSpace) +import Data.Versions import Control.Applicative (Alternative((<|>))) import Text.Read (readEither) import qualified GHCup.Utils.Parsers as Utils +import Text.PrettyPrint.HughesPJClass ( prettyShow ) data CompileHLSOptions = CompileHLSOptions { _jobs :: Maybe Int @@ -73,8 +76,8 @@ makeLenses ''CompileHLSOptions type CompileHLSMenu = Menu CompileHLSOptions Name -create :: KeyCombination -> CompileHLSMenu -create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields +create :: MenuKeyBindings -> [Version] -> CompileHLSMenu +create k availableGHCs = Menu.createMenu CompileGHCBox initialState "Compile HLS" validator k buttons fields where initialState = CompileHLSOptions @@ -140,16 +143,26 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields additionalValidator :: T.Text -> Either Menu.ErrorMessage [T.Text] additionalValidator = Right . T.split isSpace + targetGHCsField = + let label = "target GHC(s)" + in case NE.nonEmpty (fmap ToolVersion availableGHCs) of + Just ne -> Menu.createMultiSelectField (Common.MenuElement Common.TargetGhcEditBox) targetGHCs ne (T.pack . prettyShow) k + & Menu.fieldLabelL .~ label + & Menu.fieldHelpMsgL .~ "GHC versions to compile for (Press Enter to edit)" + & Menu.fieldStatusL .~ Menu.Invalid "No version selected" + _ -> Menu.createEditableField (Common.MenuElement Common.TargetGhcEditBox) ghcVersionTagEither targetGHCs + & Menu.fieldLabelL .~ label + & Menu.fieldHelpMsgL .~ "space separated list of GHC versions to compile for" + & Menu.fieldStatusL .~ Menu.Invalid "Invalid empty value" + fields = - [ Menu.createCheckBoxField (Common.MenuElement Common.UpdateCabalCheckBox) updateCabal + [ targetGHCsField + , Menu.createCheckBoxField (Common.MenuElement Common.UpdateCabalCheckBox) updateCabal & Menu.fieldLabelL .~ "cabal update" & Menu.fieldHelpMsgL .~ "Run 'cabal update' before the build" , Menu.createEditableField (Common.MenuElement Common.JobsEditBox) jobsV jobs & Menu.fieldLabelL .~ "jobs" & Menu.fieldHelpMsgL .~ "How many jobs to use for make" - , Menu.createEditableField (Common.MenuElement Common.TargetGhcEditBox) ghcVersionTagEither targetGHCs - & Menu.fieldLabelL .~ "target GHC(s)" - & Menu.fieldHelpMsgL .~ "space separated list of GHC versions to compile for" , Menu.createCheckBoxField (Common.MenuElement Common.SetCheckBox) setCompile & Menu.fieldLabelL .~ "set" & Menu.fieldHelpMsgL .~ "Set as active version after install" @@ -161,7 +174,7 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields & Menu.fieldHelpMsgL .~ "install in an isolated absolute directory instead of the default one" , Menu.createEditableField (Common.MenuElement Common.OvewrwiteVerEditBox) overWriteVersionParser overwriteVer & Menu.fieldLabelL .~ "overwrite version" - & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one" + & Menu.fieldHelpMsgL .~ "Allows to overwrite the finally installed VERSION with a different one. Allows to specify patterns: %v (version), %b (branch name), %h (short commit hash), %H (long commit hash), %g ('git describe' output)" , Menu.createEditableField (Common.MenuElement Common.PatchesEditBox) patchesV patches & Menu.fieldLabelL .~ "patches" & Menu.fieldHelpMsgL .~ "Either a URI to a patch (https/http/file) or Absolute path to patch directory" @@ -179,12 +192,12 @@ create k = Menu.createMenu CompileGHCBox initialState validator k buttons fields buttons = [ Menu.createButtonField (Common.MenuElement Common.OkButton) & Menu.fieldLabelL .~ "Compile" - & Menu.fieldHelpMsgL .~ "Compile HLS from source with options below" + & Menu.fieldHelpMsgL .~ "Compile HLS from source with options below\nRequired fields: target GHC(s)" ] handler :: BrickEvent Name e -> EventM Name CompileHLSMenu () handler = Menu.handlerMenu -draw :: CompileHLSMenu -> Widget Name -draw = Common.frontwardLayer "Compile HLS" . Menu.drawMenu +draw :: CompileHLSMenu -> [Widget Name] +draw = Menu.drawMenu diff --git a/lib-tui/GHCup/Brick/Widgets/Menus/Context.hs b/lib-tui/GHCup/Brick/Widgets/Menus/Context.hs index 5a5a3067..7807cecf 100644 --- a/lib-tui/GHCup/Brick/Widgets/Menus/Context.hs +++ b/lib-tui/GHCup/Brick/Widgets/Menus/Context.hs @@ -10,12 +10,12 @@ import Prelude hiding (appendFile) import Data.Versions (prettyVer) import GHCup.List ( ListResult(..) ) -import GHCup.Types (KeyCombination, Tool (..)) +import GHCup.Types (Tool (..)) import qualified GHCup.Brick.Common as Common import qualified GHCup.Brick.Widgets.Menu as Menu import GHCup.Brick.Common (Name (..)) -import GHCup.Brick.Widgets.Menu (Menu) +import GHCup.Brick.Widgets.Menu (Menu, MenuKeyBindings) import qualified Brick.Widgets.Core as Brick import qualified Brick.Widgets.Border as Border import qualified Brick.Focus as F @@ -28,8 +28,8 @@ import Data.Foldable (foldl') type ContextMenu = Menu ListResult Name -create :: ListResult -> KeyCombination -> ContextMenu -create lr exit_key = Menu.createMenu Common.ContextBox lr validator exit_key buttons [] +create :: ListResult -> MenuKeyBindings -> ContextMenu +create lr keyBindings = Menu.createMenu Common.ContextBox lr "" validator keyBindings buttons [] where advInstallButton = Menu.createButtonField (MenuElement Common.AdvanceInstallButton) @@ -59,7 +59,7 @@ draw menu = , Brick.txt " " , Brick.padRight Brick.Max $ Brick.txt "Press " - <+> Common.keyToWidget (menu ^. Menu.menuExitKeyL) + <+> Common.keyToWidget (menu ^. Menu.menuKeyBindingsL % Menu.mKbQuitL) <+> Brick.txt " to go back" ] where @@ -80,4 +80,4 @@ draw menu = Stack -> "Stack" handler :: BrickEvent Name e -> EventM Name ContextMenu () -handler = Menu.handlerMenu \ No newline at end of file +handler = Menu.handlerMenu diff --git a/lib-tui/GHCup/Brick/Widgets/Tutorial.hs b/lib-tui/GHCup/Brick/Widgets/Tutorial.hs index cc1ac680..738ff694 100644 --- a/lib-tui/GHCup/Brick/Widgets/Tutorial.hs +++ b/lib-tui/GHCup/Brick/Widgets/Tutorial.hs @@ -16,19 +16,20 @@ module GHCup.Brick.Widgets.Tutorial (draw) where import qualified GHCup.Brick.Common as Common import qualified GHCup.Brick.Attributes as Attributes +import GHCup.Types (KeyCombination(..)) import Brick ( Padding(Max), Widget(..), - (<=>)) + (<=>), (<+>)) import qualified Brick import Brick.Widgets.Center ( center ) import Prelude hiding ( appendFile ) -draw :: Widget Common.Name -draw = +draw :: KeyCombination -> Widget Common.Name +draw exitKey = let mkTextBox = Brick.hLimitPercent 70 . Brick.vBox . fmap (Brick.padRight Brick.Max) @@ -74,4 +75,5 @@ draw = ] , Brick.txt " " ]) - <=> Brick.padRight Brick.Max (Brick.txt "Press c+ctrl to exit the tutorial") + <=> (Brick.padRight Brick.Max $ + Brick.txt "Press " <+> Common.keyToWidget exitKey <+> Brick.txt " to exit the tutorial") diff --git a/lib-tui/GHCup/BrickMain.hs b/lib-tui/GHCup/BrickMain.hs index c0f91c74..f1399c15 100644 --- a/lib-tui/GHCup/BrickMain.hs +++ b/lib-tui/GHCup/BrickMain.hs @@ -15,9 +15,10 @@ This module contains the entrypoint for the brick application and nothing else. module GHCup.BrickMain where +import GHCup.List ( ListResult (..)) import GHCup.Types - ( Settings(noColor), - AppState(ghcupInfo, settings, keyBindings, loggerConfig), KeyCombination (KeyCombination) ) + ( Settings(noColor), Tool (GHC), + AppState(ghcupInfo, settings, keyBindings, loggerConfig), KeyBindings(..) ) import GHCup.Prelude.Logger ( logError ) import qualified GHCup.Brick.Actions as Actions import qualified GHCup.Brick.Common as Common @@ -28,6 +29,7 @@ import qualified GHCup.Brick.Widgets.Menus.Context as ContextMenu import qualified GHCup.Brick.Widgets.SectionList as Navigation import qualified GHCup.Brick.Widgets.Menus.AdvanceInstall as AdvanceInstall import qualified GHCup.Brick.Widgets.Menus.CompileGHC as CompileGHC +import GHCup.Brick.Widgets.Menu (MenuKeyBindings(..)) import qualified Brick import qualified Graphics.Vty as Vty @@ -52,7 +54,9 @@ brickMain s = do Right ad -> do let initial_list = Actions.constructList ad Common.defaultAppSettings Nothing current_element = Navigation.sectionListSelectedElement initial_list - exit_key = KeyCombination (Vty.KChar 'c') [Vty.MCtrl] -- bQuit . keyBindings $ s + exit_key = + let KeyBindings {..} = keyBindings s + in MenuKeyBindings { mKbUp = bUp, mKbDown = bDown, mKbQuit = bQuit} case current_element of Nothing -> do flip runReaderT s $ logError "Error building app state: empty ResultList" @@ -62,14 +66,16 @@ brickMain s = do BrickApp.app (Attributes.defaultAttributes $ noColor $ settings s) (Attributes.dimAttributes $ noColor $ settings s) + installedGHCs = fmap lVer $ + filter (\(ListResult {..}) -> lInstalled && lTool == GHC && lCross == Nothing) (Common._lr ad) initstate = AppState.BrickState ad Common.defaultAppSettings initial_list (ContextMenu.create e exit_key) (AdvanceInstall.create exit_key) - (CompileGHC.create exit_key) - (CompileHLS.create exit_key) + (CompileGHC.create exit_key installedGHCs) + (CompileHLS.create exit_key installedGHCs) (keyBindings s) Common.Navigation in Brick.defaultMain initapp initstate