gdritter repos bunyan / master src / Bunyan / App.hs
master

Tree @master (Download .tar.gz)

App.hs @master

e9ff4f5
 
 
 
 
088202f
e9ff4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
088202f
 
 
 
 
 
 
 
e9ff4f5
 
088202f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9ff4f5
088202f
e9ff4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
088202f
 
e9ff4f5
 
 
 
088202f
 
 
 
 
 
 
 
 
 
 
 
 
e9ff4f5
 
 
 
 
 
 
088202f
e9ff4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
088202f
 
 
 
 
e9ff4f5
088202f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e9ff4f5
 
 
 
 
 
088202f
e9ff4f5
088202f
 
 
e9ff4f5
 
 
 
 
088202f
 
e9ff4f5
088202f
 
e9ff4f5
 
 
 
 
 
 
 
088202f
 
 
 
 
 
e9ff4f5
 
088202f
 
e9ff4f5
088202f
 
e9ff4f5
 
 
 
 
 
 
 
 
 
 
 
 
 
{-# 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 }