123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- {-# LANGUAGE OverloadedStrings #-}
- module Bunyan.App (runApp) where
- import qualified Brick
- import qualified Data.Foldable as F
- import qualified Data.Map as M
- import Data.Monoid ((<>))
- import qualified Data.Sequence as S
- import qualified Data.Text as T
- import qualified Graphics.Vty as Vty
- import qualified Bunyan.Log as Log
- data Modal
- = AddSectionModal ()
- | ConfirmQuitModal
- deriving (Eq, Show)
- data AnnotEntry = AnnotEntry
- { aeAnnot :: Maybe Annotation
- , aeEntry :: Log.Entry
- } deriving (Eq, Show)
- data Annotation
- = Annotation T.Text
- | Skip
- deriving (Eq, Show)
- data EntryZipper = EntryZipper
- { ezBefore :: S.Seq AnnotEntry
- , ezAfter :: S.Seq AnnotEntry
- , ezCurrent :: AnnotEntry
- } deriving (Eq, Show)
- toZipper :: S.Seq Log.Entry -> EntryZipper
- toZipper sq
- | car S.:< cdr <- S.viewl sq = EntryZipper
- { ezBefore = S.empty
- , ezCurrent = AnnotEntry Nothing car
- , ezAfter = fmap (AnnotEntry Nothing) cdr
- }
- | otherwise = error "empty history"
- getCurrentCommit :: EntryZipper -> T.Text
- getCurrentCommit
- = Log.logCommit . aeEntry . ezCurrent
- zipperPrev :: EntryZipper -> EntryZipper
- zipperPrev ez = case S.viewr (ezBefore ez) of
- S.EmptyR -> ez
- cdr S.:> car -> ez
- { ezBefore = cdr
- , ezAfter = ezCurrent ez S.<| ezAfter ez
- , ezCurrent = car
- }
- zipperNext :: EntryZipper -> EntryZipper
- zipperNext ez = case S.viewl (ezAfter ez) of
- S.EmptyL -> ez
- car S.:< cdr -> ez
- { ezBefore = ezBefore ez S.|> ezCurrent ez
- , ezAfter = cdr
- , ezCurrent = car
- }
- annotCurrent :: T.Text -> EntryZipper -> EntryZipper
- annotCurrent annot ez = ez
- { ezCurrent = (ezCurrent ez) { aeAnnot = Just (Annotation annot) } }
- skipCurrent :: EntryZipper -> EntryZipper
- skipCurrent ez = ez
- { ezCurrent = (ezCurrent ez) { aeAnnot = Just Skip } }
- data State = State
- { stateCommits :: EntryZipper
- , stateKeys :: M.Map Char T.Text
- , stateModal :: Maybe Modal
- , stateStatus :: T.Text
- } deriving (Eq, Show)
- defaultSections :: [(Char, T.Text)]
- defaultSections =
- [ ('f', "New features")
- , ('b', "Bug fixes")
- , ('p', "Package changes")
- , ('d', "Documentation changes")
- , ('i', "Performance improvements")
- ]
- newState :: S.Seq Log.Entry -> State
- newState commits = State
- { stateKeys = M.fromList defaultSections
- , stateCommits = toZipper commits
- , stateModal = Nothing
- , stateStatus = ""
- }
- zipperToSeq :: EntryZipper -> S.Seq AnnotEntry
- zipperToSeq ez =
- ezBefore ez <> S.singleton (ezCurrent ez) <> ezAfter ez
- mkSections :: EntryZipper -> M.Map T.Text (S.Seq (S.Seq T.Text))
- mkSections ez =
- let sq = zipperToSeq ez
- in M.unionsWith (<>) [ M.singleton annot (S.singleton (Log.logMessage entry))
- | AnnotEntry { aeAnnot = mbAnnot
- , aeEntry = entry
- } <- F.toList sq
- , Just (Annotation annot) <- [mbAnnot]
- ]
- runApp :: S.Seq Log.Entry -> IO (M.Map T.Text (S.Seq (S.Seq T.Text)))
- runApp entries = do
- let state = newState entries
- vty = Vty.mkVty Vty.defaultConfig
- final <- Brick.customMain vty Nothing app state
- return (mkSections (stateCommits final))
- app :: Brick.App State () ()
- app = Brick.App
- { Brick.appDraw = draw
- , Brick.appChooseCursor = Brick.showFirstCursor
- , Brick.appHandleEvent = event
- , Brick.appStartEvent = return
- , Brick.appAttrMap = \ _ -> Brick.forceAttrMap mempty
- }
- draw :: State -> [Brick.Widget ()]
- draw st
- | Just ConfirmQuitModal <- stateModal st =
- [ Brick.txt "Are you sure you want to quit? (y/n)" ]
- | otherwise =
- let cmts = stateCommits st
- in [ Brick.viewport () Brick.Vertical $ Brick.vBox $
- (map (renderEntry False) (F.toList (ezBefore cmts)) ++
- [ renderEntry True (ezCurrent cmts) ] ++
- map (renderEntry False) (F.toList (ezAfter cmts)))
- ]
- where renderEntry isFocus AnnotEntry
- { aeAnnot = annot
- , aeEntry = Log.Entry
- { Log.logMessage = msg
- , Log.logCommit = cmt
- }
- } = (if isFocus then Brick.visible else id) $ Brick.hBox
- [ if isFocus
- then Brick.txt "> "
- else Brick.txt " "
- , case annot of
- Nothing -> Brick.txt "[]"
- Just Skip -> Brick.txt "skip"
- Just (Annotation a) -> Brick.txt a
- , Brick.txt " | "
- , Brick.txt cmt
- , Brick.txt ": "
- , Brick.vBox (map Brick.txt (F.toList msg))
- ]
- type EventHandler =
- State -> Vty.Key -> [Vty.Modifier] -> Brick.EventM () (Brick.Next State)
- event :: State -> Brick.BrickEvent () () -> Brick.EventM () (Brick.Next State)
- event st (Brick.VtyEvent (Vty.EvKey key md)) =
- case stateModal st of
- Nothing -> mainEvent st key md
- Just ConfirmQuitModal -> confirmQuitEvent st key md
- Just (AddSectionModal ()) -> addSectionEvent st key md
- event st _ = Brick.continue st
- mainEvent :: EventHandler
- mainEvent st (Vty.KChar ' ') [] = do
- let commits = stateCommits st
- current = getCurrentCommit commits
- Brick.continue st
- { stateCommits = zipperNext (skipCurrent commits)
- , stateStatus = ("skipped " <> current)
- }
- mainEvent st (Vty.KChar 'q') [] =
- Brick.continue st { stateModal = Just ConfirmQuitModal }
- mainEvent st (Vty.KChar 'a') [] =
- Brick.continue st { stateModal = Just (AddSectionModal ()) }
- mainEvent st (Vty.KChar 'j') [] =
- Brick.continue st { stateCommits = zipperNext (stateCommits st) }
- mainEvent st (Vty.KChar 'k') [] =
- Brick.continue st { stateCommits = zipperPrev (stateCommits st) }
- mainEvent st (Vty.KChar c) []
- | Just annot <- M.lookup c (stateKeys st) = do
- let commits = stateCommits st
- current = getCurrentCommit commits
- Brick.continue st
- { stateCommits = zipperNext (annotCurrent annot commits)
- , stateStatus = "Added " <> current <> " to " <> annot
- }
- | otherwise =
- Brick.continue st { stateStatus = "Unknown keybindings: " <> T.pack (show c) }
- mainEvent st _ _ = Brick.continue st
- confirmQuitEvent :: EventHandler
- confirmQuitEvent st (Vty.KChar 'y') _ = Brick.halt st
- confirmQuitEvent st _ _ = Brick.continue st { stateModal = Nothing }
- addSectionEvent :: EventHandler
- addSectionEvent st _ _ = Brick.continue st { stateModal = Nothing }
|