App.hs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. {-# LANGUAGE OverloadedStrings #-}
  2. module Bunyan.App (runApp) where
  3. import qualified Brick
  4. import qualified Data.Foldable as F
  5. import qualified Data.Map as M
  6. import Data.Monoid ((<>))
  7. import qualified Data.Sequence as S
  8. import qualified Data.Text as T
  9. import qualified Graphics.Vty as Vty
  10. import qualified Bunyan.Log as Log
  11. data Modal
  12. = AddSectionModal ()
  13. | ConfirmQuitModal
  14. deriving (Eq, Show)
  15. data AnnotEntry = AnnotEntry
  16. { aeAnnot :: Maybe Annotation
  17. , aeEntry :: Log.Entry
  18. } deriving (Eq, Show)
  19. data Annotation
  20. = Annotation T.Text
  21. | Skip
  22. deriving (Eq, Show)
  23. data EntryZipper = EntryZipper
  24. { ezBefore :: S.Seq AnnotEntry
  25. , ezAfter :: S.Seq AnnotEntry
  26. , ezCurrent :: AnnotEntry
  27. } deriving (Eq, Show)
  28. toZipper :: S.Seq Log.Entry -> EntryZipper
  29. toZipper sq
  30. | car S.:< cdr <- S.viewl sq = EntryZipper
  31. { ezBefore = S.empty
  32. , ezCurrent = AnnotEntry Nothing car
  33. , ezAfter = fmap (AnnotEntry Nothing) cdr
  34. }
  35. | otherwise = error "empty history"
  36. getCurrentCommit :: EntryZipper -> T.Text
  37. getCurrentCommit
  38. = Log.logCommit . aeEntry . ezCurrent
  39. zipperPrev :: EntryZipper -> EntryZipper
  40. zipperPrev ez = case S.viewr (ezBefore ez) of
  41. S.EmptyR -> ez
  42. cdr S.:> car -> ez
  43. { ezBefore = cdr
  44. , ezAfter = ezCurrent ez S.<| ezAfter ez
  45. , ezCurrent = car
  46. }
  47. zipperNext :: EntryZipper -> EntryZipper
  48. zipperNext ez = case S.viewl (ezAfter ez) of
  49. S.EmptyL -> ez
  50. car S.:< cdr -> ez
  51. { ezBefore = ezBefore ez S.|> ezCurrent ez
  52. , ezAfter = cdr
  53. , ezCurrent = car
  54. }
  55. annotCurrent :: T.Text -> EntryZipper -> EntryZipper
  56. annotCurrent annot ez = ez
  57. { ezCurrent = (ezCurrent ez) { aeAnnot = Just (Annotation annot) } }
  58. skipCurrent :: EntryZipper -> EntryZipper
  59. skipCurrent ez = ez
  60. { ezCurrent = (ezCurrent ez) { aeAnnot = Just Skip } }
  61. data State = State
  62. { stateCommits :: EntryZipper
  63. , stateKeys :: M.Map Char T.Text
  64. , stateModal :: Maybe Modal
  65. , stateStatus :: T.Text
  66. } deriving (Eq, Show)
  67. defaultSections :: [(Char, T.Text)]
  68. defaultSections =
  69. [ ('f', "New features")
  70. , ('b', "Bug fixes")
  71. , ('p', "Package changes")
  72. , ('d', "Documentation changes")
  73. , ('i', "Performance improvements")
  74. ]
  75. newState :: S.Seq Log.Entry -> State
  76. newState commits = State
  77. { stateKeys = M.fromList defaultSections
  78. , stateCommits = toZipper commits
  79. , stateModal = Nothing
  80. , stateStatus = ""
  81. }
  82. zipperToSeq :: EntryZipper -> S.Seq AnnotEntry
  83. zipperToSeq ez =
  84. ezBefore ez <> S.singleton (ezCurrent ez) <> ezAfter ez
  85. mkSections :: EntryZipper -> M.Map T.Text (S.Seq (S.Seq T.Text))
  86. mkSections ez =
  87. let sq = zipperToSeq ez
  88. in M.unionsWith (<>) [ M.singleton annot (S.singleton (Log.logMessage entry))
  89. | AnnotEntry { aeAnnot = mbAnnot
  90. , aeEntry = entry
  91. } <- F.toList sq
  92. , Just (Annotation annot) <- [mbAnnot]
  93. ]
  94. runApp :: S.Seq Log.Entry -> IO (M.Map T.Text (S.Seq (S.Seq T.Text)))
  95. runApp entries = do
  96. let state = newState entries
  97. vty = Vty.mkVty Vty.defaultConfig
  98. final <- Brick.customMain vty Nothing app state
  99. return (mkSections (stateCommits final))
  100. app :: Brick.App State () ()
  101. app = Brick.App
  102. { Brick.appDraw = draw
  103. , Brick.appChooseCursor = Brick.showFirstCursor
  104. , Brick.appHandleEvent = event
  105. , Brick.appStartEvent = return
  106. , Brick.appAttrMap = \ _ -> Brick.forceAttrMap mempty
  107. }
  108. draw :: State -> [Brick.Widget ()]
  109. draw st
  110. | Just ConfirmQuitModal <- stateModal st =
  111. [ Brick.txt "Are you sure you want to quit? (y/n)" ]
  112. | otherwise =
  113. let cmts = stateCommits st
  114. in [ Brick.viewport () Brick.Vertical $ Brick.vBox $
  115. (map (renderEntry False) (F.toList (ezBefore cmts)) ++
  116. [ renderEntry True (ezCurrent cmts) ] ++
  117. map (renderEntry False) (F.toList (ezAfter cmts)))
  118. ]
  119. where renderEntry isFocus AnnotEntry
  120. { aeAnnot = annot
  121. , aeEntry = Log.Entry
  122. { Log.logMessage = msg
  123. , Log.logCommit = cmt
  124. }
  125. } = (if isFocus then Brick.visible else id) $ Brick.hBox
  126. [ if isFocus
  127. then Brick.txt "> "
  128. else Brick.txt " "
  129. , case annot of
  130. Nothing -> Brick.txt "[]"
  131. Just Skip -> Brick.txt "skip"
  132. Just (Annotation a) -> Brick.txt a
  133. , Brick.txt " | "
  134. , Brick.txt cmt
  135. , Brick.txt ": "
  136. , Brick.vBox (map Brick.txt (F.toList msg))
  137. ]
  138. type EventHandler =
  139. State -> Vty.Key -> [Vty.Modifier] -> Brick.EventM () (Brick.Next State)
  140. event :: State -> Brick.BrickEvent () () -> Brick.EventM () (Brick.Next State)
  141. event st (Brick.VtyEvent (Vty.EvKey key md)) =
  142. case stateModal st of
  143. Nothing -> mainEvent st key md
  144. Just ConfirmQuitModal -> confirmQuitEvent st key md
  145. Just (AddSectionModal ()) -> addSectionEvent st key md
  146. event st _ = Brick.continue st
  147. mainEvent :: EventHandler
  148. mainEvent st (Vty.KChar ' ') [] = do
  149. let commits = stateCommits st
  150. current = getCurrentCommit commits
  151. Brick.continue st
  152. { stateCommits = zipperNext (skipCurrent commits)
  153. , stateStatus = ("skipped " <> current)
  154. }
  155. mainEvent st (Vty.KChar 'q') [] =
  156. Brick.continue st { stateModal = Just ConfirmQuitModal }
  157. mainEvent st (Vty.KChar 'a') [] =
  158. Brick.continue st { stateModal = Just (AddSectionModal ()) }
  159. mainEvent st (Vty.KChar 'j') [] =
  160. Brick.continue st { stateCommits = zipperNext (stateCommits st) }
  161. mainEvent st (Vty.KChar 'k') [] =
  162. Brick.continue st { stateCommits = zipperPrev (stateCommits st) }
  163. mainEvent st (Vty.KChar c) []
  164. | Just annot <- M.lookup c (stateKeys st) = do
  165. let commits = stateCommits st
  166. current = getCurrentCommit commits
  167. Brick.continue st
  168. { stateCommits = zipperNext (annotCurrent annot commits)
  169. , stateStatus = "Added " <> current <> " to " <> annot
  170. }
  171. | otherwise =
  172. Brick.continue st { stateStatus = "Unknown keybindings: " <> T.pack (show c) }
  173. mainEvent st _ _ = Brick.continue st
  174. confirmQuitEvent :: EventHandler
  175. confirmQuitEvent st (Vty.KChar 'y') _ = Brick.halt st
  176. confirmQuitEvent st _ _ = Brick.continue st { stateModal = Nothing }
  177. addSectionEvent :: EventHandler
  178. addSectionEvent st _ _ = Brick.continue st { stateModal = Nothing }