module Main where import Data.Maybe ( fromJust ) import Control.Applicative ( (<$>) ) import Control.Monad ( when ) import Control.Monad.Trans ( liftIO ) import Control.Monad.State ( StateT, get, modify, evalStateT ) import Text.Regex.PCRE.Light.Char8 ( Regex, compile ) import Graphics.Vty ( Event(..), Key(..), Vty, Attr , mkVty, shutdown, terminal, next_event, reserve_display , pic_for_image, update, with_fore_color, with_back_color , def_attr, blue, bright_white, bright_yellow, bright_green , black, yellow, red ) import Graphics.Vty.Widgets.Base ( (<-->) , (<++>) , hFill ) import Graphics.Vty.Widgets.Rendering ( Widget(..) , mkImage ) import Graphics.Vty.Widgets.Text ( simpleText, wrap, highlight , prepareText, textWidget, (&.&) ) import Graphics.Vty.Widgets.Borders ( bordered, hBorder ) import Graphics.Vty.Widgets.Composed ( bottomPadded ) import Graphics.Vty.Widgets.List ( List, mkList, pageUp, pageDown, resize , scrollUp, scrollDown, listWidget, getSelected , selectedIndex ) titleAttr :: Attr titleAttr = def_attr `with_back_color` blue `with_fore_color` bright_white boxAttr :: Attr boxAttr = def_attr `with_back_color` black `with_fore_color` bright_yellow bodyAttr :: Attr bodyAttr = def_attr `with_back_color` black `with_fore_color` bright_green selAttr :: Attr selAttr = def_attr `with_back_color` yellow `with_fore_color` black regex1 :: Regex regex1 = compile "(to|an|or|too)" [] hlAttr1 :: Attr hlAttr1 = def_attr `with_back_color` black `with_fore_color` red regex2 :: Regex regex2 = compile "(text|if|you)" [] hlAttr2 :: Attr hlAttr2 = def_attr `with_back_color` black `with_fore_color` yellow buildUi :: AppState -> Widget buildUi appst = let body = fromJust $ lookup (fst $ getSelected list) msgs currentItem = selectedIndex list + 1 footer = (simpleText titleAttr $ " " ++ (show currentItem) ++ "/" ++ (show $ length msgs) ++ " ") <++> hFill titleAttr '-' 1 msgs = theMessages appst list = theList appst formatter = wrap &.& highlight regex1 hlAttr1 &.& highlight regex2 hlAttr2 in bordered boxAttr $ listWidget list <--> hBorder titleAttr <--> (bottomPadded $ textWidget formatter $ prepareText bodyAttr body) <--> footer -- Construct the user interface based on the contents of the -- application state. uiFromState :: StateT AppState IO Widget uiFromState = buildUi <$> get -- The application state; this encapsulates what can vary based on -- user input and what is used to construct the interface. This is a -- place for widgets whose state need to be stored so they can be -- modified and used to reconstruct the interface as input is handled data AppState = AppState { theList :: List String , theMessages :: [(String, String)] } scrollListUp :: AppState -> AppState scrollListUp appst = appst { theList = scrollUp $ theList appst } scrollListDown :: AppState -> AppState scrollListDown appst = appst { theList = scrollDown $ theList appst } pageListUp :: AppState -> AppState pageListUp appst = appst { theList = pageUp $ theList appst } pageListDown :: AppState -> AppState pageListDown appst = appst { theList = pageDown $ theList appst } resizeList :: Int -> AppState -> AppState resizeList s appst = appst { theList = resize s $ theList appst } -- Process events from VTY, possibly modifying the application state. eventloop :: Vty -> StateT AppState IO Widget -> (Event -> StateT AppState IO Bool) -> StateT AppState IO () eventloop vty uiBuilder handle = do w <- uiBuilder evt <- liftIO $ do (img, _) <- mkImage vty w update vty $ pic_for_image img next_event vty next <- handle evt if next then eventloop vty uiBuilder handle else return () continue :: StateT AppState IO Bool continue = return True stop :: StateT AppState IO Bool stop = return False handleEvent :: Event -> StateT AppState IO Bool handleEvent (EvKey KUp []) = modify scrollListUp >> continue handleEvent (EvKey KDown []) = modify scrollListDown >> continue handleEvent (EvKey KPageUp []) = modify pageListUp >> continue handleEvent (EvKey KPageDown []) = modify pageListDown >> continue handleEvent (EvKey (KASCII 'q') []) = stop handleEvent (EvResize _ h) = do let newSize = ceiling (0.05 * fromIntegral h) when (newSize > 0) $ modify (resizeList newSize) continue handleEvent _ = continue -- Construct the application state using the message map. mkAppState :: [(String, String)] -> AppState mkAppState messages = let list = mkList bodyAttr selAttr 5 labelWidgets labelWidgets = zip labels $ map mkWidget labels mkWidget = simpleText bodyAttr labels = map fst messages in AppState { theList = list , theMessages = messages } main :: IO () main = do vty <- mkVty -- The data that we'll present in the interface. let messages = [ ("First", "This text is long enough that it will get wrapped \ \if you resize your terminal to something small. \ \It also contains enough text to get truncated at \ \the bottom if the display area is too small.\n\n\n" ) , ("Second", "the second message") , ("Third", "the third message") , ("Fourth", "the fourth message") , ("Fifth", "the fifth message") , ("Sixth", "the sixth message") , ("Seventh", "the seventh message") ] evalStateT (eventloop vty uiFromState handleEvent) $ mkAppState messages -- Clear the screen. reserve_display $ terminal vty shutdown vty