hascard 0.2.0.0 → 0.2.1.0
raw patch · 22 files changed
+1194/−878 lines, 22 filesdep +megaparsecdep −parsecPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: megaparsec
Dependencies removed: parsec
API changes (from Hackage documentation)
- Types: GlobalState :: GenIO -> Bool -> Maybe Int -> GlobalState
- Types: [_doShuffle] :: GlobalState -> Bool
- Types: [_mwc] :: GlobalState -> GenIO
- Types: [_subset] :: GlobalState -> Maybe Int
- Types: data GlobalState
- Types: doShuffle :: Lens' GlobalState Bool
- Types: mwc :: Lens' GlobalState GenIO
- Types: subset :: Lens' GlobalState (Maybe Int)
- UI: doShuffle :: Lens' GlobalState Bool
- UI: getRecents :: IO (Stack FilePath)
- UI: getRecentsFile :: IO FilePath
- UI: getShowControls :: IO Bool
- UI: getShowHints :: IO Bool
- UI: mwc :: Lens' GlobalState GenIO
- UI: runCardSelectorUI :: GlobalState -> IO ()
- UI: runCardsUI :: GlobalState -> [Card] -> IO State
- UI: runCardsWithOptions :: GlobalState -> [Card] -> IO ()
- UI: runMainMenuUI :: GlobalState -> IO ()
- UI: runSettingsUI :: IO ()
- UI: subset :: Lens' GlobalState (Maybe Int)
- UI.CardSelector: runCardSelectorUI :: GlobalState -> IO ()
- UI.CardSelector: runCardsWithOptions :: GlobalState -> [Card] -> IO ()
- UI.Cards: runCardsUI :: GlobalState -> [Card] -> IO State
- UI.FileBrowser: runFileBrowserUI :: IO (Maybe ([Card], FilePath))
- UI.Info: runInfoUI :: IO ()
- UI.MainMenu: runMainMenuUI :: GlobalState -> IO ()
- UI.Settings: getShowControls :: IO Bool
- UI.Settings: getShowHints :: IO Bool
- UI.Settings: getUseEscapeCode :: IO Bool
- UI.Settings: runSettingsUI :: IO ()
+ DeckHandling: doChunking :: Chunk -> [Card] -> [Card]
+ DeckHandling: doRandomization :: GlobalState -> [Card] -> IO [Card]
+ DeckHandling: makeChunksOfSizes :: [Int] -> [a] -> [[a]]
+ DeckHandling: splitIntoNChunks :: Int -> [a] -> [[a]]
+ Glue: drawUI :: GlobalState -> [Widget Name]
+ Glue: globalApp :: App GlobalState Event Name
+ Glue: handleAttrMap :: GlobalState -> AttrMap
+ Glue: handleEvent :: GlobalState -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ Parser: errorBundlePretty :: (Stream s, ShowErrorComponent e) => ParseErrorBundle s e -> String
+ Recents: addRecent :: FilePath -> IO ()
+ Recents: clampRecents :: FilePath -> IO (Stack FilePath)
+ Recents: duplicates :: Eq a => [a] -> [Int]
+ Recents: getPairsWithValue :: Eq a => a -> [(Int, a)] -> [(Int, a)]
+ Recents: getRecents :: IO (Stack FilePath)
+ Recents: getRecentsFile :: IO FilePath
+ Recents: initLast :: [a] -> ([a], a)
+ Recents: parseRecents :: String -> Stack FilePath
+ Recents: removeDeletedFiles :: FilePath -> IO (Stack FilePath)
+ Recents: shortenFilepaths :: [FilePath] -> [FilePath]
+ Recents: writeRecents :: Stack FilePath -> IO ()
+ Runners: cardSelectorState :: IO State
+ Runners: cardsState :: [Card] -> IO State
+ Runners: cardsWithOptionsState :: GlobalState -> [Card] -> IO State
+ Runners: entryFilter :: Bool -> FileInfo -> Bool
+ Runners: fileBrowserState :: IO State
+ Runners: infoState :: State
+ Runners: mainMenuState :: State
+ Runners: safeHead :: [a] -> Maybe a
+ Settings: defaultSettings :: Settings
+ Settings: getMaxRecents :: IO Int
+ Settings: getSettings :: IO Settings
+ Settings: getSettingsFile :: IO FilePath
+ Settings: getShowControls :: IO Bool
+ Settings: getShowHints :: IO Bool
+ Settings: getUseEscapeCode :: IO Bool
+ Settings: mkForm :: Settings -> Form Settings e Name
+ Settings: naturalNumberField :: (Ord n, Show n) => Lens' s Int -> n -> Text -> s -> FormFieldState s e n
+ Settings: parseSettings :: String -> Maybe Settings
+ Settings: renderNaturalNumber :: Text -> n -> Bool -> Int -> Widget n
+ Settings: renderYesno :: Text -> n -> Bool -> Bool -> Widget n
+ Settings: setSettings :: Settings -> IO ()
+ Settings: settingsState :: IO State
+ Settings: yesnoField :: (Ord n, Show n) => Lens' s Bool -> n -> Text -> s -> FormFieldState s e n
+ Stack: pop :: Ord a => Stack a -> Stack a
+ Stack: popWithInfo :: Ord a => Stack a -> (Stack a, a, a)
+ Stack: safeHead :: Stack a -> Maybe a
+ Stack: singleton :: a -> Stack a
+ Stack: takeStack :: Ord a => Int -> Stack a -> Stack a
+ StateManagement: getMode :: State -> Mode
+ StateManagement: getState :: GlobalState -> State
+ StateManagement: goToModeOrQuit :: GlobalState -> Mode -> EventM n (Next GlobalState)
+ StateManagement: goToState :: GlobalState -> State -> GlobalState
+ StateManagement: moveToModeOrQuit :: GlobalState -> Mode -> EventM n (Next GlobalState)
+ StateManagement: moveToModeOrQuit' :: (State -> IO State) -> GlobalState -> Mode -> EventM n (Next GlobalState)
+ StateManagement: moveToState :: GlobalState -> State -> GlobalState
+ StateManagement: popState :: GlobalState -> GlobalState
+ StateManagement: refreshRecents :: CSS -> IO CSS
+ StateManagement: safeGetState :: GlobalState -> Maybe State
+ StateManagement: updateCS :: GlobalState -> CS -> GlobalState
+ StateManagement: updateCSS :: GlobalState -> CSS -> GlobalState
+ StateManagement: updateFBS :: GlobalState -> FBS -> GlobalState
+ StateManagement: updateIS :: GlobalState -> IS -> GlobalState
+ StateManagement: updateInfo :: GlobalState -> IS -> GlobalState
+ StateManagement: updateMMS :: GlobalState -> MMS -> GlobalState
+ StateManagement: updateSS :: GlobalState -> SS -> GlobalState
+ StateManagement: updateState :: GlobalState -> State -> GlobalState
+ States: CS :: [Card] -> Int -> Int -> Card -> CardState -> Bool -> Bool -> CS
+ States: CSS :: List Name String -> Maybe String -> Stack FilePath -> Int -> CSS
+ States: CardSelector :: Mode
+ States: CardSelectorState :: CSS -> State
+ States: Cards :: Mode
+ States: CardsState :: CS -> State
+ States: Chunk :: Int -> Int -> Chunk
+ States: ControlsField :: Name
+ States: DefinitionState :: Bool -> CardState
+ States: EscapeCodeField :: Name
+ States: FBS :: FileBrowser Name -> Maybe String -> [Card] -> Maybe FilePath -> Bool -> FBS
+ States: FileBrowser :: Mode
+ States: FileBrowserState :: FBS -> State
+ States: FormState :: Bool -> Bool -> Bool -> Int -> Settings
+ States: GlobalState :: GenIO -> Bool -> Maybe Int -> Chunk -> Stack Mode -> Map Mode State -> GlobalState
+ States: HintsField :: Name
+ States: Info :: Mode
+ States: InfoState :: IS -> State
+ States: MMS :: List Name String -> MMS
+ States: MainMenu :: Mode
+ States: MainMenuState :: MMS -> State
+ States: MaxRecentsField :: Name
+ States: MultipleAnswerState :: Int -> Map Int Bool -> Int -> Bool -> CardState
+ States: MultipleChoiceState :: Int -> Int -> Map Int Bool -> CardState
+ States: OpenQuestionState :: Map Int String -> Int -> Int -> Bool -> Map Int Bool -> CardState
+ States: Ordinary :: Name
+ States: ReorderState :: Int -> Bool -> Map Int (Int, String) -> Bool -> Int -> CardState
+ States: Settings :: Mode
+ States: SettingsState :: SS -> State
+ States: [_cardState] :: CS -> CardState
+ States: [_cards] :: CS -> [Card]
+ States: [_chunk] :: GlobalState -> Chunk
+ States: [_controls] :: Settings -> Bool
+ States: [_correctGaps] :: CardState -> Map Int Bool
+ States: [_currentCard] :: CS -> Card
+ States: [_doShuffle] :: GlobalState -> Bool
+ States: [_entered] :: CardState -> Bool
+ States: [_escapeCode] :: Settings -> Bool
+ States: [_exception'] :: FBS -> Maybe String
+ States: [_exception] :: CSS -> Maybe String
+ States: [_fb] :: FBS -> FileBrowser Name
+ States: [_filePath] :: FBS -> Maybe FilePath
+ States: [_flipped] :: CardState -> Bool
+ States: [_gapInput] :: CardState -> Map Int String
+ States: [_grabbed] :: CardState -> Bool
+ States: [_highlighted] :: CardState -> Int
+ States: [_hints] :: Settings -> Bool
+ States: [_index] :: CS -> Int
+ States: [_l] :: MMS -> List Name String
+ States: [_list] :: CSS -> List Name String
+ States: [_maxRecentsToShow] :: CSS -> Int
+ States: [_maxRecents] :: Settings -> Int
+ States: [_mwc] :: GlobalState -> GenIO
+ States: [_nCards] :: CS -> Int
+ States: [_number] :: CardState -> Int
+ States: [_order] :: CardState -> Map Int (Int, String)
+ States: [_parsedCards] :: FBS -> [Card]
+ States: [_recents] :: CSS -> Stack FilePath
+ States: [_selected] :: CardState -> Map Int Bool
+ States: [_showControls] :: CS -> Bool
+ States: [_showHidden] :: FBS -> Bool
+ States: [_showHints] :: CS -> Bool
+ States: [_stack] :: GlobalState -> Stack Mode
+ States: [_states] :: GlobalState -> Map Mode State
+ States: [_subset] :: GlobalState -> Maybe Int
+ States: [_tried] :: CardState -> Map Int Bool
+ States: cardState :: Lens' CS CardState
+ States: cards :: Lens' CS [Card]
+ States: chunk :: Lens' GlobalState Chunk
+ States: controls :: Lens' Settings Bool
+ States: correctGaps :: Traversal' CardState (Map Int Bool)
+ States: currentCard :: Lens' CS Card
+ States: data CS
+ States: data CSS
+ States: data CardState
+ States: data Chunk
+ States: data FBS
+ States: data GlobalState
+ States: data Mode
+ States: data Name
+ States: data Settings
+ States: data State
+ States: defaultCardState :: Card -> CardState
+ States: doShuffle :: Lens' GlobalState Bool
+ States: entered :: Traversal' CardState Bool
+ States: escapeCode :: Lens' Settings Bool
+ States: exception :: Lens' CSS (Maybe String)
+ States: exception' :: Lens' FBS (Maybe String)
+ States: fb :: Lens' FBS (FileBrowser Name)
+ States: filePath :: Lens' FBS (Maybe FilePath)
+ States: flipped :: Traversal' CardState Bool
+ States: gapInput :: Traversal' CardState (Map Int String)
+ States: grabbed :: Traversal' CardState Bool
+ States: highlighted :: Traversal' CardState Int
+ States: hints :: Lens' Settings Bool
+ States: index :: Lens' CS Int
+ States: instance GHC.Classes.Eq States.Mode
+ States: instance GHC.Classes.Eq States.Name
+ States: instance GHC.Classes.Ord States.Mode
+ States: instance GHC.Classes.Ord States.Name
+ States: instance GHC.Read.Read States.Chunk
+ States: instance GHC.Read.Read States.Settings
+ States: instance GHC.Show.Show States.Chunk
+ States: instance GHC.Show.Show States.Mode
+ States: instance GHC.Show.Show States.Name
+ States: instance GHC.Show.Show States.Settings
+ States: l :: Lens' MMS (List Name String)
+ States: list :: Lens' CSS (List Name String)
+ States: maxRecents :: Lens' Settings Int
+ States: maxRecentsToShow :: Lens' CSS Int
+ States: mwc :: Lens' GlobalState GenIO
+ States: nCards :: Lens' CS Int
+ States: newtype MMS
+ States: number :: Traversal' CardState Int
+ States: order :: Traversal' CardState (Map Int (Int, String))
+ States: parsedCards :: Lens' FBS [Card]
+ States: recents :: Lens' CSS (Stack FilePath)
+ States: selected :: Traversal' CardState (Map Int Bool)
+ States: showControls :: Lens' CS Bool
+ States: showHidden :: Lens' FBS Bool
+ States: showHints :: Lens' CS Bool
+ States: stack :: Lens' GlobalState (Stack Mode)
+ States: states :: Lens' GlobalState (Map Mode State)
+ States: subset :: Lens' GlobalState (Maybe Int)
+ States: tried :: Traversal' CardState (Map Int Bool)
+ States: type Event = ()
+ States: type IS = ()
+ States: type SS = Form Settings Event Name
+ UI: Chunk :: Int -> Int -> Chunk
+ UI: [_chunk] :: GlobalState -> Chunk
+ UI: [_stack] :: GlobalState -> Stack Mode
+ UI: [_states] :: GlobalState -> Map Mode State
+ UI: cardSelectorState :: IO State
+ UI: cardsState :: [Card] -> IO State
+ UI: cardsWithOptionsState :: GlobalState -> [Card] -> IO State
+ UI: data Card
+ UI: data Chunk
+ UI: entryFilter :: Bool -> FileInfo -> Bool
+ UI: fileBrowserState :: IO State
+ UI: goToState :: GlobalState -> State -> GlobalState
+ UI: infoState :: State
+ UI: mainMenuState :: State
+ UI: safeHead :: [a] -> Maybe a
+ UI.Attributes: correctChoiceAttr :: AttrName
+ UI.Attributes: correctElementAttr :: AttrName
+ UI.Attributes: correctGapAttr :: AttrName
+ UI.Attributes: correctOptAttr :: AttrName
+ UI.Attributes: exceptionAttr :: AttrName
+ UI.Attributes: gapAttr :: AttrName
+ UI.Attributes: grabbedElementAttr :: AttrName
+ UI.Attributes: highlightedChoiceAttr :: AttrName
+ UI.Attributes: highlightedElementAttr :: AttrName
+ UI.Attributes: highlightedOptAttr :: AttrName
+ UI.Attributes: incorrectChoiceAttr :: AttrName
+ UI.Attributes: incorrectElementAttr :: AttrName
+ UI.Attributes: incorrectGapAttr :: AttrName
+ UI.Attributes: incorrectOptAttr :: AttrName
+ UI.Attributes: selectedAttr :: AttrName
+ UI.Attributes: selectedOptAttr :: AttrName
+ UI.Attributes: shuffledAttr :: AttrName
+ UI.Attributes: textboxAttr :: AttrName
+ UI.Attributes: theMap :: AttrMap
+ UI.Attributes: titleAttr :: AttrName
+ UI.BrickHelpers: centerPopup :: Widget n -> Widget n
+ UI.BrickHelpers: drawException :: Maybe String -> Widget n
+ UI.BrickHelpers: hFill :: Char -> Widget n
+ UI.CardSelector: data State
+ UI.CardSelector: drawUI :: GlobalState -> CSS -> [Widget Name]
+ UI.CardSelector: handleEvent :: GlobalState -> CSS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.CardSelector: theMap :: AttrMap
+ UI.Cards: CardSelectorState :: CSS -> State
+ UI.Cards: CardsState :: CS -> State
+ UI.Cards: FileBrowserState :: FBS -> State
+ UI.Cards: InfoState :: IS -> State
+ UI.Cards: MainMenuState :: MMS -> State
+ UI.Cards: SettingsState :: SS -> State
+ UI.Cards: data State
+ UI.Cards: drawUI :: CS -> [Widget Name]
+ UI.Cards: handleEvent :: GlobalState -> CS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.Cards: theMap :: AttrMap
+ UI.FileBrowser: data State
+ UI.FileBrowser: drawUI :: FBS -> [Widget Name]
+ UI.FileBrowser: handleEvent :: GlobalState -> FBS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.FileBrowser: theMap :: AttrMap
+ UI.Info: data State
+ UI.Info: drawUI :: IS -> [Widget Name]
+ UI.Info: handleEvent :: GlobalState -> IS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.Info: theMap :: AttrMap
+ UI.MainMenu: CardSelectorState :: CSS -> State
+ UI.MainMenu: CardsState :: CS -> State
+ UI.MainMenu: FileBrowserState :: FBS -> State
+ UI.MainMenu: InfoState :: IS -> State
+ UI.MainMenu: MainMenuState :: MMS -> State
+ UI.MainMenu: SettingsState :: SS -> State
+ UI.MainMenu: data State
+ UI.MainMenu: drawUI :: MMS -> [Widget Name]
+ UI.MainMenu: handleEvent :: GlobalState -> MMS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.MainMenu: theMap :: AttrMap
+ UI.Settings: data State
+ UI.Settings: drawUI :: SS -> [Widget Name]
+ UI.Settings: handleEvent :: GlobalState -> SS -> BrickEvent Name Event -> EventM Name (Next GlobalState)
+ UI.Settings: theMap :: AttrMap
- Parser: parseCards :: String -> Either ParseError [Card]
+ Parser: parseCards :: String -> Either (ParseErrorBundle String Void) [Card]
- UI: GlobalState :: GenIO -> Bool -> Maybe Int -> GlobalState
+ UI: GlobalState :: GenIO -> Bool -> Maybe Int -> Chunk -> Stack Mode -> Map Mode State -> GlobalState
Files
- ChangeLog.md +11/−0
- app/Main.hs +22/−14
- hascard.cabal +13/−5
- src/DeckHandling.hs +29/−0
- src/Glue.hs +46/−0
- src/Parser.hs +23/−27
- src/Recents.hs +102/−0
- src/Runners.hs +81/−0
- src/Settings.hs +129/−0
- src/Stack.hs +20/−1
- src/StateManagement.hs +87/−0
- src/States.hs +162/−0
- src/Types.hs +0/−11
- src/UI.hs +12/−7
- src/UI/Attributes.hs +84/−0
- src/UI/BrickHelpers.hs +31/−0
- src/UI/CardSelector.hs +59/−200
- src/UI/Cards.hs +165/−314
- src/UI/FileBrowser.hs +34/−66
- src/UI/Info.hs +37/−31
- src/UI/MainMenu.hs +21/−79
- src/UI/Settings.hs +26/−123
ChangeLog.md view
@@ -1,4 +1,15 @@ # Changelog for hascard+## 0.2.1.0+New:+- A certain chunk of a deck of cards can be reviewed using the `-c i/n` CLI option. For example `hascard -c 3/5` will split the deck into 5 chunks, and review the 3rd one. These chunks can be used with the shuffle and amount options too.+- Visual indicator in the deck selector menu for whether the deck is being shuffled or not.+- Shuffling can be toggled inside the deck selector menu using the 's' key.+- Most parsing error messages now show up as a pop-up box with nicer formattting than before.+- The maximum number of recently selected decks stored and shown can now be changed in the settings menu.++Fixed bugs:+- Flickering when switching between menus is gone. This was done by merging the seperate Brick applications into one.+ ## 0.2.0.0 New: - A new type of card is available: reorder the elements. This could break previous definition or open question cards if they had the same format as the new reorder the elements card. In that case change the 1. 2. etc. to something like 1), 2) which is not seen as a reorder type card.
app/Main.hs view
@@ -4,7 +4,6 @@ import UI import Control.Exception (displayException, try) import Control.Monad (void, when)-import Data.Functor (($>)) import Data.Version (showVersion) import Lens.Micro.Platform import Paths_hascard (version)@@ -13,13 +12,16 @@ import System.Directory (makeAbsolute) import System.FilePath (takeExtension) import System.Process (runCommand)-import System.Random.MWC (createSystemRandom, GenIO)+import System.Random.MWC (createSystemRandom)+import qualified Data.Map.Strict as Map (empty)+import qualified Stack data Opts = Opts- { _optFile :: Maybe String- , _optSubset :: Int- , _optShuffle :: Bool- , _optVersion :: Bool+ { _optFile :: Maybe String+ , _optSubset :: Int+ , _optChunk :: Chunk+ , _optShuffle :: Bool+ , _optVersion :: Bool } makeLenses ''Opts@@ -38,12 +40,13 @@ opts = Opts <$> optional (argument str (metavar "FILE" <> help "A .txt file containing flashcards")) <*> option auto (long "amount" <> short 'a' <> metavar "n" <> help "Use the first n cards in the deck (most useful combined with shuffle)" <> value (-1))+ <*> option auto (long "chunk" <> short 'c' <> metavar "i/n" <> help "Split the deck into n chunks, and review the i'th one. Counting starts at 1." <> value (Chunk 1 1)) <*> switch (long "shuffle" <> short 's' <> help "Randomize card order") <*> switch (long "version" <> short 'v' <> help "Show version number") optsWithHelp :: ParserInfo Opts optsWithHelp = info (opts <**> helper) $- fullDesc <> progDesc "Run the normal application without argument, or run it directly on a deck of flashcards by providing a text file."+ fullDesc <> progDesc "Run the normal application without argument, or run it directly on a deck of flashcards by providing a text file. Options work either way." <> header "Hascard - a TUI for reviewing notes" nothingIf :: (a -> Bool) -> a -> Maybe a@@ -54,22 +57,27 @@ run :: Opts -> IO () run opts = run' (opts ^. optFile) where- mkGlobalState gen = GlobalState {_mwc=gen, _doShuffle=opts^.optShuffle, _subset=nothingIf (<0) (opts^.optSubset) }- run' Nothing = createSystemRandom >>= runBrickFlashcards . mkGlobalState+ mkGlobalState gen = GlobalState {_mwc=gen, _doShuffle=opts^.optShuffle, _subset=nothingIf (<0) (opts^.optSubset), _states=Map.empty, _stack=Stack.empty, _chunk=opts^.optChunk }+ run' Nothing = createSystemRandom >>= start Nothing . mkGlobalState run' (Just file) = do- let filepath = + let filepath = case takeExtension file of "" -> Just $ file <> ".txt" ".txt" -> Just file _ -> Nothing case filepath of- Nothing -> putStrLn "Incorrect file type, provide a .txt file" + Nothing -> putStrLn "Incorrect file type, provide a .txt file" Just textfile -> do valOrExc <- try (readFile textfile) :: IO (Either IOError String) case valOrExc of Left exc -> putStrLn (displayException exc) Right val -> case parseCards val of- Left parseError -> print parseError- Right result -> + Left parseError -> putStr (errorBundlePretty parseError)+ Right result -> do gen <- createSystemRandom- (makeAbsolute textfile >>= addRecent) *> runCardsWithOptions (mkGlobalState gen) result $> ()+ makeAbsolute textfile >>= addRecent+ start (Just result) (mkGlobalState gen)++start :: Maybe [Card] -> GlobalState -> IO ()+start Nothing gs = runBrickFlashcards (gs `goToState` mainMenuState)+start (Just cards) gs = runBrickFlashcards =<< (gs `goToState`) <$> cardsWithOptionsState gs cards
hascard.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: e6105cfe9b053345660f9321206316f1bf88e4f94f56b7cde5625a5b521fc74b+-- hash: 217faba3ee5ad528be6ef3a365d0be4b6fcd322a10bead39904ac669a0869a05 name: hascard-version: 0.2.0.0+version: 0.2.1.0 synopsis: A TUI for reviewing notes using 'flashcards' written with markdown-like syntax. description: Hascard is a text-based user interface for reviewing notes using flashcards. Cards are written in markdown-like syntax. Please see the README file on GitHub at <https://github.com/Yvee1/hascard#readme> for more information. category: Application@@ -30,10 +30,18 @@ library exposed-modules: Debug+ DeckHandling+ Glue Parser+ Recents+ Runners+ Settings Stack+ StateManagement+ States Types UI+ UI.Attributes UI.BrickHelpers UI.Cards UI.CardSelector@@ -51,12 +59,12 @@ , containers >0.6.0 && <0.7 , directory >=1.3.3 && <1.4 , filepath >=1.4.2 && <1.5+ , megaparsec >=8.0.0 && <8.1 , microlens >=0.4.11 && <0.5 , microlens-platform >=0.4.1 && <0.5 , mwc-random >=0.14 && <0.15 , optparse-applicative >=0.15.1 && <0.16 , ordered-containers >=0.2.2 && <0.3- , parsec >=3.1.13 && <3.2 , process >=1.6.5 && <1.7 , random-fu >=0.2.7 && <0.3 , strict >=0.3.2 && <0.4@@ -80,12 +88,12 @@ , directory >=1.3.3 && <1.4 , filepath >=1.4.2 && <1.5 , hascard+ , megaparsec >=8.0.0 && <8.1 , microlens >=0.4.11 && <0.5 , microlens-platform >=0.4.1 && <0.5 , mwc-random >=0.14 && <0.15 , optparse-applicative >=0.15.1 && <0.16 , ordered-containers >=0.2.2 && <0.3- , parsec >=3.1.13 && <3.2 , process >=1.6.5 && <1.7 , random-fu >=0.2.7 && <0.3 , strict >=0.3.2 && <0.4@@ -110,12 +118,12 @@ , directory >=1.3.3 && <1.4 , filepath >=1.4.2 && <1.5 , hascard+ , megaparsec >=8.0.0 && <8.1 , microlens >=0.4.11 && <0.5 , microlens-platform >=0.4.1 && <0.5 , mwc-random >=0.14 && <0.15 , optparse-applicative >=0.15.1 && <0.16 , ordered-containers >=0.2.2 && <0.3- , parsec >=3.1.13 && <3.2 , process >=1.6.5 && <1.7 , random-fu >=0.2.7 && <0.3 , strict >=0.3.2 && <0.4
+ src/DeckHandling.hs view
@@ -0,0 +1,29 @@+module DeckHandling where+import Data.Random+import Lens.Micro.Platform+import States+import Types++doRandomization :: GlobalState -> [Card] -> IO [Card]+doRandomization state cards = + let n = length cards in do+ cards' <- if state^.doShuffle then sampleFrom (state^.mwc) (shuffleN n cards) else return cards+ return $ maybe cards' (`take` cards') (state^.subset)++doChunking :: Chunk -> [Card] -> [Card]+doChunking (Chunk i n) cards = + splitIntoNChunks n cards !! (i-1)++splitIntoNChunks :: Int -> [a] -> [[a]]+splitIntoNChunks n xs =+ let (q, r) = length xs `quotRem` n+ qs = replicate n q+ rs = replicate r 1 ++ repeat 0+ chunkSizes = zipWith (+) qs rs+ in makeChunksOfSizes chunkSizes xs++makeChunksOfSizes :: [Int] -> [a] -> [[a]]+makeChunksOfSizes [] _ = []+makeChunksOfSizes (n:ns) xs = + let (chunk, rest) = splitAt n xs+ in chunk : makeChunksOfSizes ns rest
+ src/Glue.hs view
@@ -0,0 +1,46 @@+module Glue where+import Brick+import States+import StateManagement+import qualified UI.MainMenu as MM+import qualified UI.Settings as S+import qualified UI.Info as I+import qualified UI.CardSelector as CS+import qualified UI.FileBrowser as FB+import qualified UI.Cards as C++globalApp :: App GlobalState Event Name+globalApp = App+ { appDraw = drawUI+ , appChooseCursor = showFirstCursor+ , appHandleEvent = handleEvent+ , appStartEvent = return+ , appAttrMap = handleAttrMap+ }++drawUI :: GlobalState -> [Widget Name]+drawUI gs = case getState gs of+ MainMenuState s -> MM.drawUI s+ SettingsState s -> S.drawUI s+ InfoState s -> I.drawUI s+ CardSelectorState s -> CS.drawUI gs s+ FileBrowserState s -> FB.drawUI s+ CardsState s -> C.drawUI s++handleEvent :: GlobalState -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs ev = case getState gs of+ MainMenuState s -> MM.handleEvent gs s ev+ SettingsState s -> S.handleEvent gs s ev+ InfoState s -> I.handleEvent gs s ev+ CardSelectorState s -> CS.handleEvent gs s ev+ FileBrowserState s -> FB.handleEvent gs s ev+ CardsState s -> C.handleEvent gs s ev++handleAttrMap :: GlobalState -> AttrMap+handleAttrMap gs = case getState gs of+ MainMenuState _ -> MM.theMap+ SettingsState _ -> S.theMap+ InfoState _ -> I.theMap+ CardSelectorState _ -> CS.theMap+ FileBrowserState _ -> FB.theMap+ CardsState _ -> C.theMap
src/Parser.hs view
@@ -1,16 +1,20 @@-{-# LANGUAGE DataKinds, ExistentialQuantification, GADTs, KindSignatures #-}-module Parser (parseCards) where+{-# LANGUAGE DataKinds, ExistentialQuantification, GADTs, KindSignatures, OverloadedStrings #-}+module Parser (parseCards, errorBundlePretty) where +import Data.Void import qualified Data.List.NonEmpty as NE-import Text.Parsec+import Text.Megaparsec+import Text.Megaparsec.Char import Types +type Parser = Parsec Void String+ uncurry3 f (a, b, c) = f a b c -parseCards :: String -> Either ParseError [Card]+parseCards :: String -> Either (ParseErrorBundle String Void) [Card] parseCards = parse pCards "failed when parsing cards" -pCards = pCard `sepEndBy1` seperator+pCards = (pCard `sepEndBy1` seperator) <* eof pCard = uncurry3 MultipleChoice<$> try pMultChoice <|> uncurry MultipleAnswer <$> try pMultAnswer <|> uncurry Reorder <$> try pReorder@@ -20,8 +24,8 @@ pHeader = do many eol char '#'- spaces- many notEOL+ spaceChar+ many (noneOf ['\n', '\r']) pMultChoice = do header <- pHeader@@ -31,9 +35,9 @@ return (header, correct, incorrects) pChoice = do- kind <- oneOf "*-"- space- text <- manyTill anyChar $ lookAhead (try (try choicePrefix <|> seperator <|> eof'))+ kind <- oneOf ['*','-']+ spaceChar+ text <- manyTill anySingle $ lookAhead (try (try choicePrefix <|> seperator <|> eof')) return (kind, text) choicePrefix = string "- "@@ -47,9 +51,9 @@ pOption = do char '['- kind <- oneOf "*x "+ kind <- oneOf ['*','x',' '] string "] "- text <- manyTill anyChar $ lookAhead (try (seperator <|> string "[" <|> eof'))+ text <- manyTill anySingle $ lookAhead (try (seperator <|> string "[" <|> eof')) return $ makeOption kind text pReorder = do@@ -64,11 +68,11 @@ pReorderElement = do int <- pReorderPrefix- text <- manyTill anyChar $ lookAhead (try (try seperator <|> try pReorderPrefix <|> eof'))+ text <- manyTill anySingle $ lookAhead (try (try seperator <|> try pReorderPrefix <|> eof')) return (read int, text) pReorderPrefix = do- int <- many1 digit+ int <- some digitChar string ". " return int @@ -87,13 +91,13 @@ (pre, gap) <- pGap Perforated pre gap <$> pSentence -chars = escaped <|> anyChar+chars = escaped <|> anySingle escaped = char '\\' >> char '_' pGap = do pre <- manyTill chars $ lookAhead (try gappedSpecialChars) char '_'- gaps <- manyTill (noneOf "_|") (lookAhead (try gappedSpecialChars)) `sepBy1` string "|"+ gaps <- manyTill (noneOf ['_','|']) (lookAhead (try gappedSpecialChars)) `sepBy1` string "|" char '_' return (pre, NE.fromList gaps) @@ -102,7 +106,7 @@ <|> string "_" pNormal = do- text <- manyTill (noneOf "_") $ lookAhead $ try $ gappedSpecialChars <|> eof'+ text <- manyTill (noneOf ['_']) $ lookAhead $ try $ gappedSpecialChars <|> eof' return (Normal text) pDef = do@@ -111,12 +115,6 @@ descr <- manyTill chars $ lookAhead $ try $ seperator <|> eof' return (header, descr) -eol = try (string "\n\r")- <|> try (string "\r\n")- <|> string "\n"- <|> string "\r"- <?> "end of line"- eof' = eof >> return [] <?> "end of file" seperator = do@@ -124,8 +122,6 @@ many eol return sep -notEOL = noneOf "\n\r"- makeMultipleChoice :: [(Char, String)] -> (CorrectOption, [IncorrectOption]) makeMultipleChoice options = makeMultipleChoice' [] [] 0 options where@@ -138,5 +134,5 @@ makeOption :: Char -> String -> Option makeOption kind text- | kind `elem` "*x" = Option Correct text- | otherwise = Option Incorrect text+ | kind `elem` ['*','x'] = Option Correct text+ | otherwise = Option Incorrect text
+ src/Recents.hs view
@@ -0,0 +1,102 @@+module Recents where+import Control.Monad (filterM)+import Data.List (sort)+import Settings+import Stack (Stack)+import System.Environment (lookupEnv)+import System.FilePath ((</>), splitFileName, dropExtension, splitPath, joinPath)+import qualified Stack as S+import qualified System.Directory as D+import qualified System.IO.Strict as IOS (readFile)++getRecents :: IO (Stack FilePath)+getRecents = do+ rf <- getRecentsFile+ exists <- D.doesFileExist rf+ if exists+ then removeDeletedFiles rf *> clampRecents rf+ else return S.empty++removeDeletedFiles :: FilePath -> IO (Stack FilePath)+removeDeletedFiles fp = do+ contents <- IOS.readFile fp+ existing <- S.fromList <$> filterM D.doesFileExist (lines contents)+ writeRecents existing+ return existing++parseRecents :: String -> Stack FilePath+parseRecents = S.fromList . lines++clampRecents :: FilePath -> IO (Stack FilePath)+clampRecents fp = do+ rs <- parseRecents <$> IOS.readFile fp+ maxRs <- getMaxRecents+ let clamped = S.takeStack maxRs rs+ writeRecents clamped+ return clamped++addRecent :: FilePath -> IO ()+addRecent fp = do+ rs <- getRecents+ maxRecents <- getMaxRecents+ let rs' = fp `S.insert` rs + rs'' = if S.size rs' <= maxRecents+ then rs'+ else S.removeLast rs'+ writeRecents rs''++writeRecents :: Stack FilePath -> IO ()+writeRecents stack = do+ file <- getRecentsFile+ writeFile file $ unlines (S.toList stack)++getRecentsFile :: IO FilePath+getRecentsFile = do+ maybeSnap <- lookupEnv "SNAP_USER_DATA"+ xdg <- D.getXdgDirectory D.XdgData "hascard"++ let dir = case maybeSnap of+ Just path | not (null path) -> path+ | otherwise -> xdg+ Nothing -> xdg+ D.createDirectoryIfMissing True dir++ return (dir </> "recents")++initLast :: [a] -> ([a], a)+initLast [x] = ([], x)+initLast (x:xs) = let (xs', y) = initLast xs+ in (x:xs', y)++shortenFilepaths :: [FilePath] -> [FilePath]+shortenFilepaths fps = uncurry shortenFilepaths' (unzip (map ((\(pre, fn) -> (pre, dropExtension fn)) . splitFileName) fps))+ where+ shortenFilepaths' prefixes abbreviations =+ let ds = duplicates abbreviations in+ if null ds then abbreviations else+ shortenFilepaths' + (flip map (zip [0..] prefixes) (+ \(i, pre) -> if i `elem` ds then+ joinPath (init (splitPath pre)) else pre+ ))+ (flip map (zip [0..] abbreviations) (+ \(i, abbr) -> if i `elem` ds then + last (splitPath (prefixes !! i)) ++ abbr+ else abbr) )+ ++duplicates :: Eq a => [a] -> [Int]+duplicates = sort . map fst . duplicates' 0 [] []+ where duplicates' _ _ acc [] = acc+ duplicates' i seen acc (x:xs) = duplicates' (i+1) ((i, x) : seen) acc' xs+ where acc' = case (getPairsWithValue x acc, getPairsWithValue x seen) of+ ([], []) -> acc+ ([], ys) -> (i, x) : ys ++ acc+ (_, _) -> (i, x) : acc+ -- acc' = if getPairsWithValue x seen then (i, x) : acc else acc ++getPairsWithValue :: Eq a => a -> [(Int, a)] -> [(Int, a)]+getPairsWithValue _ [] = []+getPairsWithValue y ((i, x):xs)+ | x == y = (i, x) : getPairsWithValue y xs+ | otherwise = getPairsWithValue y xs
+ src/Runners.hs view
@@ -0,0 +1,81 @@+module Runners where+import Brick.Widgets.FileBrowser+import DeckHandling+import Data.Maybe (fromMaybe)+import Recents+import Lens.Micro.Platform+import Settings+import States+import Types+import qualified Brick.Widgets.List as L+import qualified Data.Vector as Vec+import qualified Stack as S++cardSelectorState :: IO State+cardSelectorState = do+ rs <- getRecents+ maxRs <- getMaxRecents+ let prettyRecents = shortenFilepaths (S.toList rs)+ options = Vec.fromList (prettyRecents ++ ["Select file from system"])+ initialState = CSS+ { _list = L.list Ordinary options 1+ , _exception = Nothing+ , _recents = rs+ , _maxRecentsToShow = maxRs }+ return $ CardSelectorState initialState++mainMenuState :: State+mainMenuState = + let options = Vec.fromList + [ "Select"+ , "Info"+ , "Settings"+ , "Quit" ]++ initialState = MMS (L.list Ordinary options 1) in+ MainMenuState initialState++safeHead :: [a] -> Maybe a+safeHead [] = Nothing+safeHead (x:_) = Just x++cardsState :: [Card] -> IO State+cardsState deck = do+ hints <- getShowHints+ controls <- getShowControls++ let mFirstCard = safeHead deck+ firstCard = fromMaybe (Definition "Empty deck" "Click enter to go back.") mFirstCard+ deck' = maybe [firstCard] (const deck) mFirstCard++ initialState = + CS { _cards = deck'+ , _index = 0+ , _currentCard = firstCard+ , _cardState = defaultCardState firstCard+ , _nCards = length deck'+ , _showHints = hints+ , _showControls = controls }+ + return $ CardsState initialState++cardsWithOptionsState :: GlobalState -> [Card] -> IO State+cardsWithOptionsState gs cards = + let chunked = doChunking (gs^.chunk) cards+ in doRandomization gs chunked >>= cardsState++infoState :: State+infoState = InfoState ()++fileBrowserState :: IO State+fileBrowserState = do+ browser <- newFileBrowser selectNonDirectories Ordinary Nothing+ let filteredBrowser = setFileBrowserEntryFilter (Just (entryFilter False)) browser+ return $ FileBrowserState (FBS filteredBrowser Nothing [] Nothing False)++entryFilter :: Bool -> FileInfo -> Bool+entryFilter acceptHidden info = fileExtensionMatch "txt" info && (acceptHidden || + case fileInfoFilename info of+ ".." -> True+ '.' : _ -> False+ _ -> True)
+ src/Settings.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE OverloadedStrings, RankNTypes #-}+module Settings where+import Brick+import Brick.Forms+import UI.BrickHelpers+import Data.Char (isDigit)+import States+import Data.Maybe+import System.FilePath ((</>))+import System.Environment (lookupEnv)+import Text.Read (readMaybe)+import Lens.Micro.Platform+import qualified Data.Text as T+import qualified Graphics.Vty as V+import qualified System.Directory as D++getShowHints :: IO Bool+getShowHints = do+ settings <- getSettings+ return $ settings ^. hints++getShowControls :: IO Bool+getShowControls = do+ settings <- getSettings+ return $ settings ^. controls++getUseEscapeCode :: IO Bool+getUseEscapeCode = do+ settings <- getSettings+ return $ settings ^. escapeCode++getMaxRecents :: IO Int+getMaxRecents = do+ settings <- getSettings+ return $ settings ^. maxRecents++getSettings :: IO Settings+getSettings = do+ sf <- getSettingsFile+ exists <- D.doesFileExist sf+ if exists + then do+ maybeSettings <- parseSettings <$> readFile sf+ maybe (return defaultSettings) return maybeSettings+ else return defaultSettings++parseSettings :: String -> Maybe Settings+parseSettings = readMaybe++getSettingsFile :: IO FilePath+getSettingsFile = do+ maybeSnap <- lookupEnv "SNAP_USER_DATA"+ xdg <- D.getXdgDirectory D.XdgConfig "hascard"++ let dir = case maybeSnap of+ Just path | not (null path) -> path+ | otherwise -> xdg+ Nothing -> xdg+ D.createDirectoryIfMissing True dir+ return (dir </> "settings")++defaultSettings :: Settings+defaultSettings = FormState { _hints=False, _controls=True, _escapeCode=False, _maxRecents=5}++setSettings :: Settings -> IO ()+setSettings settings = do+ sf <- getSettingsFile+ writeFile sf (show settings)++settingsState :: IO State+settingsState = SettingsState . mkForm <$> getSettings++mkForm :: Settings -> Form Settings e Name+mkForm =+ let label s w = padBottom (Pad 1) $ padRight (Pad 2) (strWrap s) <+> w+ + in newForm+ [ label "Draw hints using underscores for definition cards" @@= yesnoField hints HintsField ""+ , label "Show controls at the bottom of screen" @@= yesnoField controls ControlsField ""+ , label "Use the '-n \\e[5 q' escape code to change the cursor to a blinking line on start" @@= yesnoField escapeCode EscapeCodeField ""+ , label "Maximum number of recently selected files stored" @@= hLimit 3 @@= naturalNumberField maxRecents MaxRecentsField "" ]++yesnoField :: (Ord n, Show n) => Lens' s Bool -> n -> T.Text -> s -> FormFieldState s e n+yesnoField stLens name label initialState =+ let initVal = initialState ^. stLens++ handleEvent (MouseDown n _ _ _) s | n == name = return $ not s+ handleEvent (VtyEvent (V.EvKey (V.KChar ' ') [])) s = return $ not s+ handleEvent (VtyEvent (V.EvKey V.KEnter [])) s = return $ not s+ handleEvent _ s = return s+ + in FormFieldState { formFieldState = initVal+ , formFields = [ FormField name Just True + (renderYesno label name)+ handleEvent ]+ , formFieldLens = stLens+ , formFieldRenderHelper = id+ , formFieldConcat = vBox }++renderYesno :: T.Text -> n -> Bool -> Bool -> Widget n+renderYesno label n foc val =+ let addAttr = if foc then withDefAttr focusedFormInputAttr else id+ in clickable n $ (if val then addAttr (txt "Yes") else addAttr (txt "No") <+> txt " ") <+> txt label++naturalNumberField :: (Ord n, Show n) => Lens' s Int -> n -> T.Text -> s -> FormFieldState s e n+naturalNumberField stLens name label initialState =+ let initVal = initialState ^. stLens+ -- clamp s = ++ handleEvent (VtyEvent (V.EvKey (V.KChar c) [])) s | isDigit c = return $ if s < 100 then read (show s ++ [c]) else s+ handleEvent (VtyEvent (V.EvKey V.KBS [])) s = return $ case show s of+ "" -> 0+ xs -> fromMaybe 0 (readMaybe (init xs))+ handleEvent _ s = return s+ + in FormFieldState { formFieldState = initVal+ , formFields = [ FormField name Just True + (renderNaturalNumber label name)+ handleEvent ]+ , formFieldLens = stLens+ , formFieldRenderHelper = id+ , formFieldConcat = vBox }++renderNaturalNumber :: T.Text -> n -> Bool -> Int -> Widget n+renderNaturalNumber label n foc val =+ let addAttr = if foc then withDefAttr focusedFormInputAttr else id+ val' = show val+ csr = if foc then showCursor n (Location (length val',0)) else id+ in csr (addAttr (str val')) <+> txt label <+> hFill ' '
src/Stack.hs view
@@ -9,6 +9,9 @@ empty :: Stack a empty = OS.empty +singleton :: a -> Stack a+singleton = OS.singleton+ insert :: Ord a => a -> Stack a -> Stack a insert = (|<) @@ -18,14 +21,30 @@ head :: Stack a -> a head = (`unsafeElemAt` 0) +safeHead :: Stack a -> Maybe a+safeHead = (`elemAt` 0)+ last :: Stack a -> a last s = s `unsafeElemAt` (Stack.size s - 1) +pop :: Ord a => Stack a -> Stack a+pop s = OS.delete (Stack.head s) s++popWithInfo :: Ord a => Stack a -> (Stack a, a, a)+popWithInfo s = let+ top = Stack.head s+ s' = OS.delete top s+ top' = Stack.head s' in+ (s', top, top')+ tail :: Ord a => Stack a -> [a]-tail s = toList $ OS.delete (Stack.head s) s+tail = toList . pop elemAt :: Stack a -> Int -> Maybe a elemAt = OS.elemAt++takeStack :: Ord a => Int -> Stack a -> Stack a+takeStack n = fromList . take n . toList unsafeElemAt :: Stack a -> Int -> a unsafeElemAt s = fromJust . OS.elemAt s
+ src/StateManagement.hs view
@@ -0,0 +1,87 @@+module StateManagement where+import Brick+import Control.Monad.IO.Class+import Data.Maybe (fromJust)+import Lens.Micro.Platform+import Recents+import States hiding (cardState)+import Stack hiding (head)+import qualified Brick.Widgets.List as L+import qualified Data.Vector as Vec+import qualified Data.Map.Strict as M+import qualified Stack++getMode :: State -> Mode+getMode (MainMenuState _) = MainMenu+getMode (SettingsState _) = Settings+getMode (InfoState _) = Info+getMode (CardSelectorState _) = CardSelector+getMode (FileBrowserState _) = FileBrowser+getMode (CardsState _) = Cards++getState :: GlobalState -> State+getState = fromJust . safeGetState++updateState :: GlobalState -> State -> GlobalState+updateState gs s = gs & states %~ M.insert (getMode s) s++updateMMS :: GlobalState -> MMS -> GlobalState+updateMMS gs s = updateState gs (MainMenuState s)++updateSS :: GlobalState -> SS -> GlobalState+updateSS gs s = updateState gs (SettingsState s)++updateIS :: GlobalState -> IS -> GlobalState+updateIS gs s = updateState gs (InfoState s)++updateCS :: GlobalState -> CS -> GlobalState+updateCS gs s = updateState gs (CardsState s)++updateCSS :: GlobalState -> CSS -> GlobalState+updateCSS gs s = updateState gs (CardSelectorState s)++updateInfo :: GlobalState -> IS -> GlobalState+updateInfo gs s = updateState gs (InfoState s)++updateFBS :: GlobalState -> FBS -> GlobalState+updateFBS gs s = updateState gs (FileBrowserState s)++goToState :: GlobalState -> State -> GlobalState+goToState gs s = gs & states %~ M.insert (getMode s) s+ & stack %~ insert (getMode s)++moveToState :: GlobalState -> State -> GlobalState +moveToState gs = goToState (popState gs)++popState :: GlobalState -> GlobalState+popState gs = let+ s = gs ^. stack+ top = Stack.head s+ s' = Stack.pop s in+ gs & states %~ M.delete top+ & stack .~ s'++safeGetState :: GlobalState -> Maybe State+safeGetState gs = do+ key <- safeHead (gs ^. stack)+ M.lookup key (gs ^. states)++goToModeOrQuit :: GlobalState -> Mode -> EventM n (Next GlobalState)+goToModeOrQuit gs mode = + maybe (halt gs) (continue . goToState gs) $ M.lookup mode (gs ^. states) ++moveToModeOrQuit :: GlobalState -> Mode -> EventM n (Next GlobalState)+moveToModeOrQuit gs mode = + maybe (halt gs) (continue . moveToState gs) $ M.lookup mode (gs ^. states) ++moveToModeOrQuit' :: (State -> IO State) -> GlobalState -> Mode -> EventM n (Next GlobalState)+moveToModeOrQuit' f gs mode = + maybe (halt gs) (\s -> continue . moveToState gs =<< liftIO (f s)) $ M.lookup mode (gs ^. states) ++refreshRecents :: CSS -> IO CSS+refreshRecents s = do+ rs <- getRecents+ let prettyRecents = shortenFilepaths (toList rs)+ options = Vec.fromList (prettyRecents ++ ["Select file from system"])+ return $ s & recents .~ rs+ & list .~ L.list Ordinary options 1
+ src/States.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE TemplateHaskell #-}+module States where++import Brick.Forms (Form)+import Brick.Widgets.FileBrowser+import Brick.Widgets.List (List)+import Data.Char (isDigit)+import Data.Map.Strict (Map)+import Lens.Micro.Platform+import System.Random.MWC (GenIO)+import Stack hiding (head)+import Types+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as M++data Name = HintsField+ | ControlsField+ | EscapeCodeField+ | MaxRecentsField+ | Ordinary+ deriving (Eq, Ord, Show)+type Event = ()++data Mode = MainMenu + | Settings + | Info + | CardSelector+ | FileBrowser + | Cards+ deriving (Show, Eq, Ord)++data State = MainMenuState MMS+ | SettingsState SS+ | InfoState IS+ | CardSelectorState CSS+ | FileBrowserState FBS+ | CardsState CS++data Chunk = Chunk Int Int++instance Show Chunk where+ show (Chunk i n) = show i <> "/" <> show n++instance Read Chunk where+ readsPrec _ input =+ let (i', rest1) = span isDigit input+ i = read i' :: Int+ (c:rest2) = rest1+ (n', rest3) = span isDigit rest2+ n = read n' :: Int+ in [(Chunk i n, rest3) | c `elem` ['/', ' '] && n >= i && i >= 1] ++data GlobalState = GlobalState+ { _mwc :: GenIO+ , _doShuffle :: Bool+ , _subset :: Maybe Int+ , _chunk :: Chunk+ , _stack :: Stack Mode+ , _states :: Map Mode State+ }++data CardState = + DefinitionState+ { _flipped :: Bool }+ | MultipleChoiceState+ { _highlighted :: Int+ , _number :: Int+ , _tried :: Map Int Bool -- indices of tried choices+ }+ | MultipleAnswerState+ { _highlighted :: Int+ , _selected :: Map Int Bool+ , _number :: Int+ , _entered :: Bool+ }+ | OpenQuestionState+ { _gapInput :: Map Int String+ , _highlighted :: Int+ , _number :: Int+ , _entered :: Bool+ , _correctGaps :: Map Int Bool+ }+ | ReorderState+ { _highlighted :: Int+ , _grabbed :: Bool + , _order :: Map Int (Int, String)+ , _entered :: Bool+ , _number :: Int+ }++defaultCardState :: Card -> CardState+defaultCardState Definition{} = DefinitionState { _flipped = False }+defaultCardState (MultipleChoice _ _ ics) = MultipleChoiceState + { _highlighted = 0+ , _number = length ics + 1+ , _tried = M.fromList [(i, False) | i <- [0..length ics]] }+defaultCardState (OpenQuestion _ perforated) = OpenQuestionState + { _gapInput = M.empty+ , _highlighted = 0+ , _number = nGapsInPerforated perforated+ , _entered = False+ , _correctGaps = M.fromList [(i, False) | i <- [0..nGapsInPerforated perforated - 1]] }+defaultCardState (MultipleAnswer _ answers) = MultipleAnswerState + { _highlighted = 0+ , _selected = M.fromList [(i, False) | i <- [0..NE.length answers-1]]+ , _entered = False+ , _number = NE.length answers }+defaultCardState (Reorder _ elements) = ReorderState+ { _highlighted = 0+ , _grabbed = False+ , _order = M.fromList (zip [0..] (NE.toList elements))+ , _entered = False+ , _number = NE.length elements }++data CS = CS+ { _cards :: [Card] -- list of flashcards+ , _index :: Int -- current card index+ , _nCards :: Int -- number of cards+ , _currentCard :: Card+ , _cardState :: CardState+ , _showHints :: Bool+ , _showControls :: Bool+ -- , _incorrectCards :: [Int] -- list of indices of incorrect answers+ }++newtype MMS = MMS + { _l :: List Name String }++type IS = ()++data Settings = FormState+ { _hints :: Bool+ , _controls :: Bool+ , _escapeCode :: Bool+ , _maxRecents :: Int }+ deriving (Read, Show)++type SS = Form Settings Event Name++data CSS = CSS+ { _list :: List Name String+ , _exception :: Maybe String+ , _recents :: Stack FilePath+ , _maxRecentsToShow :: Int+ }++data FBS = FBS+ { _fb :: FileBrowser Name+ , _exception' :: Maybe String+ , _parsedCards :: [Card]+ , _filePath :: Maybe FilePath+ , _showHidden :: Bool+ }++makeLenses ''State+makeLenses ''MMS+makeLenses ''GlobalState+makeLenses ''CardState+makeLenses ''CS+makeLenses ''Settings+makeLenses ''CSS+makeLenses ''FBS
src/Types.hs view
@@ -1,16 +1,6 @@-{-# LANGUAGE TemplateHaskell #-} module Types where import Data.List.NonEmpty (NonEmpty)-import Lens.Micro.Platform-import System.Random.MWC (GenIO) -data GlobalState = GlobalState- { _mwc :: GenIO- , _doShuffle :: Bool- , _subset :: Maybe Int }--makeLenses ''GlobalState- -- Word Description data Card = Definition String String | OpenQuestion String Perforated@@ -25,7 +15,6 @@ question :: String, elements :: NonEmpty (Int, String) }- deriving Show data Type = Incorrect | Correct
src/UI.hs view
@@ -1,10 +1,15 @@-module UI (module X, module UI) where+module UI (module X, runBrickFlashcards, GlobalState(..), Chunk(..), Card, goToState) where -import UI.Cards as X (runCardsUI)-import UI.CardSelector as X-import UI.MainMenu as X (runMainMenuUI)-import UI.Settings as X-import Types as X (GlobalState (..), mwc, doShuffle, subset)+import UI.CardSelector as X (addRecent)+import Settings as X (getUseEscapeCode)+import Runners as X+import Brick+import Glue+import States+import StateManagement+import Types (Card) runBrickFlashcards :: GlobalState -> IO ()-runBrickFlashcards = runMainMenuUI+runBrickFlashcards gs = do+ _ <- defaultMain globalApp gs+ return ()
+ src/UI/Attributes.hs view
@@ -0,0 +1,84 @@+module UI.Attributes where+import Brick+import Brick.Forms+import Graphics.Vty++titleAttr :: AttrName+titleAttr = attrName "title"++selectedAttr :: AttrName+selectedAttr = attrName "selected"++textboxAttr :: AttrName+textboxAttr = attrName "textbox"++highlightedChoiceAttr :: AttrName+highlightedChoiceAttr = attrName "highlighted choice"++incorrectChoiceAttr :: AttrName+incorrectChoiceAttr = attrName "incorrect choice"++correctChoiceAttr :: AttrName+correctChoiceAttr = attrName "correct choice"++highlightedOptAttr :: AttrName+highlightedOptAttr = attrName "highlighted option"++selectedOptAttr :: AttrName+selectedOptAttr = attrName "selected option"++correctOptAttr :: AttrName+correctOptAttr = attrName "correct option"++incorrectOptAttr :: AttrName+incorrectOptAttr = attrName "incorrect option"++gapAttr :: AttrName+gapAttr = attrName "gap"++incorrectGapAttr :: AttrName+incorrectGapAttr = attrName "incorrect gap"++correctGapAttr :: AttrName+correctGapAttr = attrName "correct gap"++highlightedElementAttr :: AttrName+highlightedElementAttr = attrName "highlighted element"++grabbedElementAttr :: AttrName+grabbedElementAttr = attrName "grabbed element"++correctElementAttr :: AttrName+correctElementAttr = attrName "correct element"++incorrectElementAttr :: AttrName+incorrectElementAttr = attrName "incorrect element"++exceptionAttr :: AttrName+exceptionAttr = attrName "exception"++shuffledAttr :: AttrName+shuffledAttr = attrName "shuffled indicator"++theMap :: AttrMap+theMap = attrMap defAttr+ [ (titleAttr, fg yellow)+ , (textboxAttr, defAttr)+ , (highlightedChoiceAttr, fg yellow)+ , (incorrectChoiceAttr, fg red)+ , (correctChoiceAttr, fg green)+ , (incorrectGapAttr, fg red `withStyle` underline)+ , (correctGapAttr, fg green `withStyle` underline)+ , (highlightedOptAttr, fg yellow)+ , (selectedOptAttr, fg blue)+ , (incorrectOptAttr, fg red)+ , (correctOptAttr, fg green)+ , (highlightedElementAttr, fg yellow)+ , (grabbedElementAttr, fg blue)+ , (correctElementAttr, fg green)+ , (incorrectElementAttr, fg red)+ , (gapAttr, defAttr `withStyle` underline)+ , (selectedAttr, fg white `withStyle` underline)+ , (exceptionAttr, fg red)+ , (focusedFormInputAttr, defAttr `withStyle` underline)+ , (shuffledAttr, fg red) ]
src/UI/BrickHelpers.hs view
@@ -1,9 +1,12 @@ module UI.BrickHelpers where import Text.Wrap import Brick+import Brick.Widgets.Border import Brick.Widgets.Center import Data.Text (pack)+import Graphics.Vty (imageWidth, imageHeight, charFill) import Lens.Micro+import UI.Attributes hCenteredStrWrap :: String -> Widget n hCenteredStrWrap = hCenteredStrWrapWithAttr id@@ -14,3 +17,31 @@ let w = c^.availWidthL let result = vBox $ map (hCenter . attr . txt) $ wrapTextToLines (WrapSettings {preserveIndentation=False, breakLongWords=True}) w (pack p) render result++-- Somewhat inefficient because rendering is done just to+-- determine the width and height. So don't use this if the+-- rendering is expensive.+centerPopup :: Widget n -> Widget n+centerPopup widget = Widget Fixed Fixed $ do+ c <- getContext+ result <- render widget+ let w = result^.imageL.to imageWidth+ h = result^.imageL.to imageHeight+ x = (c^.availWidthL - w) `div` 2+ y = (c^.availHeightL - h) `div` 2+ render $ translateBy (Location (x, y)) widget++drawException :: Maybe String -> Widget n+drawException Nothing = emptyWidget+drawException (Just e) =+ centerPopup $ + borderWithLabel (str "Error") $+ withAttr exceptionAttr $ str e++-- | Fill all available space with the specified character. Grows only+-- horizontally.+hFill :: Char -> Widget n+hFill ch =+ Widget Greedy Fixed $ do+ c <- getContext+ return $ emptyResult & imageL .~ charFill (c^.attrL) ch (c^.availWidthL) 1
src/UI/CardSelector.hs view
@@ -1,77 +1,55 @@-{-# LANGUAGE TemplateHaskell #-} module UI.CardSelector - (runCardSelectorUI+ ( State+ , drawUI+ , handleEvent+ , theMap , getRecents , getRecentsFile- , addRecent- , runCardsWithOptions) where+ , addRecent ) where import Brick import Brick.Widgets.Border import Brick.Widgets.Border.Style import Brick.Widgets.Center import Control.Exception (displayException, try)-import Control.Monad (filterM) import Control.Monad.IO.Class-import Data.Functor (void)-import Data.List (sort)-import Data.Random import Lens.Micro.Platform import Parser-import Stack (Stack)-import System.Environment (lookupEnv)-import System.FilePath ((</>), splitFileName, dropExtension, splitPath, joinPath)-import Types+import Recents+import Runners+import States+import StateManagement+import UI.Attributes hiding (theMap) import UI.BrickHelpers-import UI.FileBrowser (runFileBrowserUI)-import UI.Cards (runCardsUI, Card) import qualified Brick.Widgets.List as L-import qualified Data.Vector as Vec import qualified Graphics.Vty as V import qualified Stack as S-import qualified System.Directory as D-import qualified System.IO.Strict as IOS (readFile)--type Event = ()-type Name = ()-data State = State- { _list :: L.List Name String- , _exception :: Maybe String- , _recents :: Stack FilePath- , _gs :: GlobalState- }--makeLenses ''State--app :: App State Event Name-app = App - { appDraw = drawUI- , appChooseCursor = neverShowCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }+import qualified UI.Attributes as A -drawUI :: State -> [Widget Name]-drawUI s = - [ drawMenu s <=> drawException s ]+drawUI :: GlobalState -> CSS -> [Widget Name]+drawUI gs s = + [ drawException (s ^. exception), drawMenu gs s ] title :: Widget Name-title = withAttr titleAttr $ hCenteredStrWrap "Select a deck of flashcards"+title = withAttr titleAttr $ str "Select a deck of flashcards " -drawMenu :: State -> Widget Name-drawMenu s = +shuffledWidget :: Bool -> Widget Name+shuffledWidget shuffled = withAttr shuffledAttr $ str $ + if shuffled then "(Shuffled)" else ""++drawMenu :: GlobalState -> CSS -> Widget Name+drawMenu gs s = joinBorders $ center $ withBorderStyle unicodeRounded $ border $ hLimitPercent 60 $- title <=>+ hCenter (title <+> shuffledWidget (gs^.doShuffle)) <=> hBorder <=> hCenter (drawList s) -drawList :: State -> Widget Name-drawList s = vLimit 6 $+drawList :: CSS -> Widget Name+drawList s = vLimit (s ^. maxRecentsToShow + 1) $ L.renderListWithIndex (drawListElement l) True l where l = s ^. list @@ -80,171 +58,52 @@ where wAttr1 = if selected then withDefAttr selectedAttr else id wAttr2 = if i == length l - 1 then withAttr lastElementAttr else id -drawException :: State -> Widget Name-drawException s = case s ^. exception of- Nothing -> emptyWidget- Just exc -> withAttr exceptionAttr $ strWrap exc--titleAttr :: AttrName-titleAttr = attrName "title"--selectedAttr :: AttrName-selectedAttr = attrName "selected"- lastElementAttr :: AttrName lastElementAttr = attrName "last element" -exceptionAttr :: AttrName-exceptionAttr = attrName "exception"- theMap :: AttrMap-theMap = attrMap V.defAttr+theMap = applyAttrMappings [ (L.listAttr, V.defAttr) , (selectedAttr, fg V.white `V.withStyle` V.underline) , (titleAttr, fg V.yellow)- , (lastElementAttr, fg V.blue)- , (exceptionAttr, fg V.red) ]--handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s@State{_list=l} (VtyEvent e) =- case e of- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s- V.EvKey V.KEsc [] -> halt s-- _ -> do l' <- L.handleListEventVi L.handleListEvent e l- let s' = (s & list .~ l') in- case e of- V.EvKey V.KEnter [] ->- case L.listSelectedElement l' of- Nothing -> continue s'- Just (_, "Select file from system") -> suspendAndResume $ runFileBrowser s'- Just (i, _) -> do- let fp = (s' ^. recents) `S.unsafeElemAt` i- fileOrExc <- liftIO (try (readFile fp) :: IO (Either IOError String))- case fileOrExc of- Left exc -> continue (s' & exception ?~ displayException exc)- Right file -> case parseCards file of- Left parseError -> continue (s' & exception ?~ show parseError)- Right result -> suspendAndResume $ do- s'' <- addRecentInternal s' fp- _ <- runCardsWithOptions (s^.gs) result- return (s'' & exception .~ Nothing)- _ -> continue s'--handleEvent l _ = continue l--runCardSelectorUI :: GlobalState -> IO ()-runCardSelectorUI gs = do- rs <- getRecents- let prettyRecents = shortenFilepaths (S.toList rs)- let options = Vec.fromList (prettyRecents ++ ["Select file from system"])- let initialState = State (L.list () options 1) Nothing rs gs- _ <- defaultMain app initialState- return () --getRecents :: IO (Stack FilePath)-getRecents = do- rf <- getRecentsFile- exists <- D.doesFileExist rf- if exists- then removeDeletedFiles rf- else return S.empty+ , (lastElementAttr, fg V.blue) ] A.theMap -removeDeletedFiles :: FilePath -> IO (Stack FilePath)-removeDeletedFiles fp = do- file <- IOS.readFile fp- existing <- S.fromList <$> filterM D.doesFileExist (lines file)- writeRecents existing- return existing+handleEvent :: GlobalState -> CSS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs s@CSS{_list=l, _exception=exc} (VtyEvent ev) =+ let update = updateCSS gs+ continue' = continue . update+ halt' = continue . popState in+ case (exc, ev) of+ (Just _, _) -> continue' $ s & exception .~ Nothing+ (_, e) -> case e of+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt' gs+ V.EvKey V.KEsc [] -> halt' gs+ V.EvKey (V.KChar 's') [] -> continue (gs & doShuffle %~ not) -maxRecents :: Int-maxRecents = 5+ _ -> do l' <- L.handleListEventVi L.handleListEvent e l+ let s' = (s & list .~ l') in+ case e of+ V.EvKey V.KEnter [] ->+ case L.listSelectedElement l' of+ Nothing -> continue' s'+ Just (_, "Select file from system") -> + let gs' = update s' in continue =<< (gs' `goToState`) <$> liftIO fileBrowserState+ Just (i, _) -> do+ let fp = (s' ^. recents) `S.unsafeElemAt` i+ fileOrExc <- liftIO (try (readFile fp) :: IO (Either IOError String))+ case fileOrExc of+ Left exc -> continue' (s' & exception ?~ displayException exc)+ Right file -> case parseCards file of+ Left parseError -> continue' (s' & exception ?~ errorBundlePretty parseError)+ Right result -> continue =<< liftIO (do+ s'' <- addRecentInternal s' fp+ let gs' = update s''+ (gs' `goToState`) <$> cardsWithOptionsState gs' result)+ _ -> continue' s' -addRecent :: FilePath -> IO ()-addRecent fp = do- rs <- getRecents- let rs' = fp `S.insert` rs - rs'' = if S.size rs' <= maxRecents- then rs'- else S.removeLast rs'- writeRecents rs''+handleEvent gs _ _ = continue gs -addRecentInternal :: State -> FilePath -> IO State+addRecentInternal :: CSS -> FilePath -> IO CSS addRecentInternal s fp = do addRecent fp refreshRecents s--writeRecents :: Stack FilePath -> IO ()-writeRecents stack = do- file <- getRecentsFile- writeFile file $ unlines (S.toList stack)--getRecentsFile :: IO FilePath-getRecentsFile = do- maybeSnap <- lookupEnv "SNAP_USER_DATA"- xdg <- D.getXdgDirectory D.XdgData "hascard"-- let dir = case maybeSnap of- Just path | not (null path) -> path- | otherwise -> xdg- Nothing -> xdg- D.createDirectoryIfMissing True dir-- return (dir </> "recents")--initLast :: [a] -> ([a], a)-initLast [x] = ([], x)-initLast (x:xs) = let (xs', y) = initLast xs- in (x:xs', y)--shortenFilepaths :: [FilePath] -> [FilePath]-shortenFilepaths fps = uncurry shortenFilepaths' (unzip (map ((\(pre, fn) -> (pre, dropExtension fn)) . splitFileName) fps))- where- shortenFilepaths' prefixes abbreviations =- let ds = duplicates abbreviations in- if null ds then abbreviations else- shortenFilepaths' - (flip map (zip [0..] prefixes) (- \(i, pre) -> if i `elem` ds then- joinPath (init (splitPath pre)) else pre- ))- (flip map (zip [0..] abbreviations) (- \(i, abbr) -> if i `elem` ds then - last (splitPath (prefixes !! i)) ++ abbr- else abbr) )- --duplicates :: Eq a => [a] -> [Int]-duplicates = sort . map fst . duplicates' 0 [] []- where duplicates' _ _ acc [] = acc- duplicates' i seen acc (x:xs) = duplicates' (i+1) ((i, x) : seen) acc' xs- where acc' = case (getPairsWithValue x acc, getPairsWithValue x seen) of- ([], []) -> acc- ([], ys) -> (i, x) : ys ++ acc- (_, _) -> (i, x) : acc- -- acc' = if getPairsWithValue x seen then (i, x) : acc else acc --getPairsWithValue :: Eq a => a -> [(Int, a)] -> [(Int, a)]-getPairsWithValue y [] = []-getPairsWithValue y ((i, x):xs)- | x == y = (i, x) : getPairsWithValue y xs- | otherwise = getPairsWithValue y xs--refreshRecents :: State -> IO State-refreshRecents s = do- rs <- getRecents- let prettyRecents = shortenFilepaths (S.toList rs)- options = Vec.fromList (prettyRecents ++ ["Select file from system"])- return $ s & recents .~ rs- & list .~ L.list () options 1--runFileBrowser :: State -> IO State-runFileBrowser s = do- result <- runFileBrowserUI- maybe (return s) (\(cards, fp) -> addRecentInternal s fp <* runCardsWithOptions (s^.gs) cards) result--runCardsWithOptions :: GlobalState -> [Card] -> IO ()-runCardsWithOptions state cards =- let n = length cards in do- cards' <- if state^.doShuffle then sampleFrom (state^.mwc) (shuffleN n cards) else return cards- void $ maybe (runCardsUI state cards') (\n -> runCardsUI state (take n cards')) (state^.subset)
src/UI/Cards.hs view
@@ -1,17 +1,18 @@-{-# LANGUAGE TemplateHaskell #-}-module UI.Cards (runCardsUI, Card) where+module UI.Cards (Card, State(..), drawUI, handleEvent, theMap) where import Brick import Lens.Micro.Platform import Types+import States+import StateManagement import Data.Char (isSpace) import Data.List (dropWhileEnd) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import Text.Wrap import Data.Text (pack)+import UI.Attributes import UI.BrickHelpers-import UI.Settings (getShowHints, getShowControls) import qualified Data.List.NonEmpty as NE import qualified Data.Text as T import qualified Data.Map.Strict as M@@ -20,107 +21,14 @@ import qualified Brick.Widgets.Center as C import qualified Graphics.Vty as V -type Event = ()-type Name = ()--data CardState = - DefinitionState- { _flipped :: Bool }- | MultipleChoiceState- { _highlighted :: Int- , _number :: Int- , _tried :: Map Int Bool -- indices of tried choices- }- | MultipleAnswerState- { _highlighted :: Int- , _selected :: Map Int Bool- , _number :: Int- , _entered :: Bool- }- | OpenQuestionState- { _gapInput :: Map Int String- , _highlighted :: Int- , _number :: Int- , _entered :: Bool- , _correctGaps :: Map Int Bool- }- | ReorderState- { _highlighted :: Int- , _grabbed :: Bool - , _order :: Map Int (Int, String)- , _entered :: Bool- , _number :: Int- }--data State = State- { _cards :: [Card] -- list of flashcards- , _index :: Int -- current card index- , _nCards :: Int -- number of cards- , _currentCard :: Card- , _cardState :: CardState- , _showHints :: Bool- , _showControls :: Bool- -- , _incorrectCards :: [Int] -- list of indices of incorrect answers- }--makeLenses ''CardState-makeLenses ''State--defaultCardState :: Card -> CardState-defaultCardState Definition{} = DefinitionState { _flipped = False }-defaultCardState (MultipleChoice _ _ ics) = MultipleChoiceState - { _highlighted = 0- , _number = length ics + 1- , _tried = M.fromList [(i, False) | i <- [0..length ics]] }-defaultCardState (OpenQuestion _ perforated) = OpenQuestionState - { _gapInput = M.empty- , _highlighted = 0- , _number = nGapsInPerforated perforated- , _entered = False- , _correctGaps = M.fromList [(i, False) | i <- [0..nGapsInPerforated perforated - 1]] }-defaultCardState (MultipleAnswer _ answers) = MultipleAnswerState - { _highlighted = 0- , _selected = M.fromList [(i, False) | i <- [0..NE.length answers-1]]- , _entered = False- , _number = NE.length answers }-defaultCardState (Reorder _ elements) = ReorderState- { _highlighted = 0- , _grabbed = False- , _order = M.fromList (zip [0..] (NE.toList elements))- , _entered = False- , _number = NE.length elements }--app :: App State Event Name-app = App - { appDraw = drawUI- , appChooseCursor = showFirstCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }--runCardsUI :: GlobalState -> [Card] -> IO State-runCardsUI gs deck = do- hints <- getShowHints- controls <- getShowControls-- let initialState = State { _cards = deck- , _index = 0- , _currentCard = head deck- , _cardState = defaultCardState (head deck)- , _nCards = length deck- , _showHints = hints- , _showControls = controls }- defaultMain app initialState- --------------------------------------------------- --------------------- DRAWING --------------------- --------------------------------------------------- -drawUI :: State -> [Widget Name]+drawUI :: CS -> [Widget Name] drawUI s = [drawCardUI s <=> drawInfo s] -drawInfo :: State -> Widget Name+drawInfo :: CS -> Widget Name drawInfo s = if not (s ^. showControls) then emptyWidget else strWrap . ("ESC: quit" <>) $ case s ^. cardState of DefinitionState {} -> ", ENTER: flip card / continue"@@ -136,22 +44,32 @@ withAttr textboxAttr $ hLimitPercent 60 w -drawProgress :: State -> Widget Name+drawProgress :: CS -> Widget Name drawProgress s = C.hCenter $ str (show (s^.index + 1) ++ "/" ++ show (s^.nCards)) -drawCardUI :: State -> Widget Name+drawCardUI :: CS -> Widget Name drawCardUI s = let p = 1 in joinBorders $ drawCardBox $ (<=> drawProgress s) $ case (s ^. cards) !! (s ^. index) of- Definition title descr -> drawHeader title <=> B.hBorder <=> padLeftRight p (drawDef s descr <=> str " ")+ Definition title descr -> drawHeader title+ <=> B.hBorder+ <=> padLeftRight p (drawDef s descr <=> str " ") - MultipleChoice question correct others -> drawHeader question <=> B.hBorder <=> padLeftRight p (drawChoices s (listMultipleChoice correct others) <=> str " ")+ MultipleChoice question correct others -> drawHeader question + <=> B.hBorder + <=> padLeftRight p (drawChoices s (listMultipleChoice correct others) <=> str " ") - OpenQuestion title perforated -> drawHeader title <=> B.hBorder <=> padLeftRight p (drawPerforated s perforated <=> str " ")+ OpenQuestion title perforated -> drawHeader title+ <=> B.hBorder+ <=> padLeftRight p (drawPerforated s perforated <=> str " ") - MultipleAnswer question options -> drawHeader question <=> B.hBorder <=> padRight (Pad p) (drawOptions s options <=> str " ")+ MultipleAnswer question options -> drawHeader question+ <=> B.hBorder+ <=> padRight (Pad p) (drawOptions s options <=> str " ") - Reorder question elements -> drawHeader question <=> B.hBorder <=> padLeftRight p (drawReorder s elements <=> str " ")+ Reorder question _ -> drawHeader question+ <=> B.hBorder+ <=> padLeftRight p (drawReorder s <=> str " ") drawHeader :: String -> Widget Name drawHeader title = withAttr titleAttr $@@ -171,15 +89,15 @@ where descr' = dropWhileEnd isSpace' descr -drawDef :: State -> String -> Widget Name+drawDef :: CS -> String -> Widget Name drawDef s def = if s ^. showHints then drawHintedDef s def else drawNormalDef s def -drawHintedDef :: State -> String -> Widget Name+drawHintedDef :: CS -> String -> Widget Name drawHintedDef s def = case s ^. cardState of DefinitionState {_flipped=f} -> if f then drawDescr def else drawDescr [if isSpace' char then char else '_' | char <- def] _ -> error "impossible: " -drawNormalDef:: State -> String -> Widget Name+drawNormalDef:: CS -> String -> Widget Name drawNormalDef s def = case s ^. cardState of DefinitionState {_flipped=f} -> if f then drawDescr def@@ -201,7 +119,7 @@ then listMultipleChoice' (cStr : opts) (i+1) c' ics else listMultipleChoice' (icStr : opts) (i+1) c' ics' -drawChoices :: State -> [String] -> Widget Name+drawChoices :: CS -> [String] -> Widget Name drawChoices s options = case (s ^. cardState, s ^. currentCard) of (MultipleChoiceState {_highlighted=i, _tried=kvs}, MultipleChoice _ (CorrectOption k _) _) -> vBox formattedOptions @@ -217,7 +135,7 @@ ] _ -> error "impossible" -drawOptions :: State -> NonEmpty Option -> Widget Name+drawOptions :: CS -> NonEmpty Option -> Widget Name drawOptions s = case (s ^. cardState, s ^. currentCard) of (MultipleAnswerState {_highlighted=j, _selected=kvs, _entered=submitted}, _) -> vBox . NE.toList . NE.map drawOption . (`NE.zip` NE.fromList [0..])@@ -235,16 +153,16 @@ _ -> error "hopefully this is never shown" -drawPerforated :: State -> Perforated -> Widget Name+drawPerforated :: CS -> Perforated -> Widget Name drawPerforated s p = drawSentence s $ perforatedToSentence p -drawSentence :: State -> Sentence -> Widget Name+drawSentence :: CS -> Sentence -> Widget Name drawSentence state sentence = Widget Greedy Fixed $ do c <- getContext let w = c^.availWidthL render $ makeSentenceWidget w state sentence -makeSentenceWidget :: Int -> State -> Sentence -> Widget Name+makeSentenceWidget :: Int -> CS -> Sentence -> Widget Name makeSentenceWidget w state = vBox . fst . makeSentenceWidget' 0 0 where makeSentenceWidget' :: Int -> Int -> Sentence -> ([Widget Name], Bool)@@ -257,7 +175,7 @@ cursor :: Widget Name -> Widget Name -- i is the index of the gap that we are drawing; j is the gap that is currently selected- cursor = if i == j then showCursor () (Location (textWidth gap, 0)) else id+ cursor = if i == j then showCursor Ordinary (Location (textWidth gap, 0)) else id correct = M.findWithDefault False i cgs coloring = case (submitted, correct) of@@ -297,8 +215,8 @@ ts' = ts & _last %~ (`T.append` postfix) in (map txt (filter (/=T.empty) ts'), textWidth (last ts'), False) -drawReorder :: State -> NonEmpty (Int, String) -> Widget Name-drawReorder s elements = case (s ^. cardState, s ^. currentCard) of+drawReorder :: CS -> Widget Name+drawReorder s = case (s ^. cardState, s ^. currentCard) of (ReorderState {_highlighted=j, _grabbed=g, _order=kvs, _number=n, _entered=submitted}, Reorder _ _) -> vBox . flip map (map (\i -> (i, kvs M.! i)) [0..n-1]) $ \(i, (k, text)) ->@@ -320,162 +238,167 @@ ---------------------------------------------------- ---------------------- Events ---------------------- ----------------------------------------------------+halt' :: GlobalState -> EventM n (Next GlobalState)+halt' = flip (moveToModeOrQuit' (\(CardSelectorState s) -> CardSelectorState <$> refreshRecents s)) CardSelector -handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s (VtyEvent e) = case e of- V.EvKey V.KEsc [] -> halt s- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s- V.EvKey V.KRight [V.MCtrl] -> next s- V.EvKey V.KLeft [V.MCtrl] -> previous s+handleEvent :: GlobalState -> CS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs s (VtyEvent e) =+ let update = updateCS gs+ continue' = continue . update in+ case e of+ V.EvKey V.KEsc [] -> halt' gs+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt' gs+ V.EvKey V.KRight [V.MCtrl] -> next gs s+ V.EvKey V.KLeft [V.MCtrl] -> previous gs s - ev -> case (s ^. cardState, s ^. currentCard) of- (DefinitionState{_flipped = f}, _) ->- case ev of- V.EvKey V.KEnter [] -> - if f- then next s - else continue $ s & cardState.flipped %~ not- _ -> continue s+ ev -> case (s ^. cardState, s ^. currentCard) of+ (DefinitionState{_flipped = f}, _) ->+ case ev of+ V.EvKey V.KEnter [] -> + if f+ then next gs s + else continue' $ s & cardState.flipped %~ not+ _ -> continue' s - (MultipleChoiceState {_highlighted = i, _number = n, _tried = kvs}, MultipleChoice _ (CorrectOption j _) _) ->- case ev of- V.EvKey V.KUp [] -> continue up- V.EvKey (V.KChar 'k') [] -> continue up- V.EvKey V.KDown [] -> continue down - V.EvKey (V.KChar 'j') [] -> continue down+ (MultipleChoiceState {_highlighted = i, _number = n, _tried = kvs}, MultipleChoice _ (CorrectOption j _) _) ->+ case ev of+ V.EvKey V.KUp [] -> continue' up+ V.EvKey (V.KChar 'k') [] -> continue' up+ V.EvKey V.KDown [] -> continue' down + V.EvKey (V.KChar 'j') [] -> continue' down - V.EvKey V.KEnter [] ->- if frozen- then next s- else continue $ s & cardState.tried %~ M.insert i True+ V.EvKey V.KEnter [] ->+ if frozen+ then next gs s+ else continue' $ s & cardState.tried %~ M.insert i True - _ -> continue s+ _ -> continue' s - where frozen = M.findWithDefault False j kvs- - down = if i < n-1 && not frozen- then s & (cardState.highlighted) +~ 1- else s+ where frozen = M.findWithDefault False j kvs+ + down = if i < n-1 && not frozen+ then s & (cardState.highlighted) +~ 1+ else s - up = if i > 0 && not frozen- then s & (cardState.highlighted) -~ 1- else s- - (MultipleAnswerState {_highlighted = i, _number = n, _entered = submitted}, MultipleAnswer {}) ->- case ev of- V.EvKey V.KUp [] -> continue up- V.EvKey (V.KChar 'k') [] -> continue up- V.EvKey V.KDown [] -> continue down - V.EvKey (V.KChar 'j') [] -> continue down+ up = if i > 0 && not frozen+ then s & (cardState.highlighted) -~ 1+ else s+ + (MultipleAnswerState {_highlighted = i, _number = n, _entered = submitted}, MultipleAnswer {}) ->+ case ev of+ V.EvKey V.KUp [] -> continue' up+ V.EvKey (V.KChar 'k') [] -> continue' up+ V.EvKey V.KDown [] -> continue' down + V.EvKey (V.KChar 'j') [] -> continue' down - V.EvKey (V.KChar 'c') [] -> continue $ s & (cardState.entered) .~ True+ V.EvKey (V.KChar 'c') [] -> continue' $ s & (cardState.entered) .~ True - V.EvKey V.KEnter [] ->- if frozen- then next s- else continue $ s & cardState.selected %~ M.adjust not i+ V.EvKey V.KEnter [] ->+ if frozen+ then next gs s+ else continue' $ s & cardState.selected %~ M.adjust not i - _ -> continue s+ _ -> continue' s - where frozen = submitted- - down = if i < n-1 && not frozen- then s & (cardState.highlighted) +~ 1- else s+ where frozen = submitted+ + down = if i < n-1 && not frozen+ then s & (cardState.highlighted) +~ 1+ else s - up = if i > 0 && not frozen- then s & (cardState.highlighted) -~ 1- else s+ up = if i > 0 && not frozen+ then s & (cardState.highlighted) -~ 1+ else s - (OpenQuestionState {_highlighted = i, _number = n, _gapInput = kvs, _correctGaps = cGaps}, OpenQuestion _ perforated) ->- let correct = M.foldr (&&) True cGaps in- case ev of- V.EvKey (V.KFun 1) [] -> continue $- s & cardState.gapInput .~ correctAnswers- & cardState.entered .~ True- & cardState.correctGaps .~ M.fromAscList [(i, True) | i <- [0..n-1]]- where correctAnswers = M.fromAscList $ zip [0..] $ map NE.head (sentenceToGaps (perforatedToSentence perforated))+ (OpenQuestionState {_highlighted = i, _number = n, _gapInput = kvs, _correctGaps = cGaps}, OpenQuestion _ perforated) ->+ let correct = M.foldr (&&) True cGaps in+ case ev of+ V.EvKey (V.KFun 1) [] -> continue' $+ s & cardState.gapInput .~ correctAnswers+ & cardState.entered .~ True+ & cardState.correctGaps .~ M.fromAscList [(i, True) | i <- [0..n-1]]+ where correctAnswers = M.fromAscList $ zip [0..] $ map NE.head (sentenceToGaps (perforatedToSentence perforated)) - V.EvKey (V.KChar '\t') [] -> continue $ - if i < n - 1 && not correct- then s & (cardState.highlighted) +~ 1- else s & (cardState.highlighted) .~ 0- - V.EvKey V.KRight [] -> continue $ - if i < n - 1 && not correct- then s & (cardState.highlighted) +~ 1- else s+ V.EvKey (V.KChar '\t') [] -> continue' $ + if i < n - 1 && not correct+ then s & (cardState.highlighted) +~ 1+ else s & (cardState.highlighted) .~ 0+ + V.EvKey V.KRight [] -> continue' $ + if i < n - 1 && not correct+ then s & (cardState.highlighted) +~ 1+ else s - V.EvKey V.KLeft [] -> continue $ - if i > 0 && not correct- then s & (cardState.highlighted) -~ 1- else s+ V.EvKey V.KLeft [] -> continue' $ + if i > 0 && not correct+ then s & (cardState.highlighted) -~ 1+ else s - V.EvKey (V.KChar c) [] -> continue $- if correct then s else s & cardState.gapInput.at i.non "" %~ (++[c])+ V.EvKey (V.KChar c) [] -> continue' $+ if correct then s else s & cardState.gapInput.at i.non "" %~ (++[c]) - V.EvKey V.KEnter [] -> if correct then next s else continue s'- where sentence = perforatedToSentence perforated- gaps = sentenceToGaps sentence+ V.EvKey V.KEnter [] -> if correct then next gs s else continue' s'+ where sentence = perforatedToSentence perforated+ gaps = sentenceToGaps sentence - s' = s & (cardState.correctGaps) %~ M.mapWithKey (\j _ -> M.findWithDefault "" j kvs `elem` gaps !! j) & (cardState.entered) .~ True+ s' = s & (cardState.correctGaps) %~ M.mapWithKey (\j _ -> M.findWithDefault "" j kvs `elem` gaps !! j) & (cardState.entered) .~ True - V.EvKey V.KBS [] -> continue $ - if correct then s else s & cardState.gapInput.ix i %~ backspace- where backspace "" = ""- backspace xs = init xs- _ -> continue s- - (ReorderState {_highlighted = i, _entered = submitted, _grabbed=dragging, _number = n }, Reorder _ _) ->- case ev of- V.EvKey V.KUp [] -> continue up- V.EvKey (V.KChar 'k') [] -> continue up- V.EvKey V.KDown [] -> continue down - V.EvKey (V.KChar 'j') [] -> continue down+ V.EvKey V.KBS [] -> continue' $ + if correct then s else s & cardState.gapInput.ix i %~ backspace+ where backspace "" = ""+ backspace xs = init xs+ _ -> continue' s+ + (ReorderState {_highlighted = i, _entered = submitted, _grabbed=dragging, _number = n }, Reorder _ _) ->+ case ev of+ V.EvKey V.KUp [] -> continue' up+ V.EvKey (V.KChar 'k') [] -> continue' up+ V.EvKey V.KDown [] -> continue' down + V.EvKey (V.KChar 'j') [] -> continue' down - V.EvKey (V.KChar 'c') [] -> continue $ s & (cardState.entered) .~ True+ V.EvKey (V.KChar 'c') [] -> continue' $ s & (cardState.entered) .~ True - V.EvKey V.KEnter [] ->- if frozen- then next s- else continue $ s & cardState.grabbed %~ not+ V.EvKey V.KEnter [] ->+ if frozen+ then next gs s+ else continue' $ s & cardState.grabbed %~ not - _ -> continue s+ _ -> continue' s - where frozen = submitted- - down = - case (frozen, i < n - 1, dragging) of- (True, _, _) -> s- (_, False, _) -> s- (_, _, False) -> s & (cardState.highlighted) +~ 1- (_, _, True) -> s & (cardState.highlighted) +~ 1- & (cardState.order) %~ interchange i (i+1)+ where frozen = submitted+ + down = + case (frozen, i < n - 1, dragging) of+ (True, _, _) -> s+ (_, False, _) -> s+ (_, _, False) -> s & (cardState.highlighted) +~ 1+ (_, _, True) -> s & (cardState.highlighted) +~ 1+ & (cardState.order) %~ interchange i (i+1) - up =- case (frozen, i > 0, dragging) of- (True, _, _) -> s- (_, False, _) -> s- (_, _, False) -> s & (cardState.highlighted) -~ 1- (_, _, True) -> s & (cardState.highlighted) -~ 1- & (cardState.order) %~ interchange i (i-1)+ up =+ case (frozen, i > 0, dragging) of+ (True, _, _) -> s+ (_, False, _) -> s+ (_, _, False) -> s & (cardState.highlighted) -~ 1+ (_, _, True) -> s & (cardState.highlighted) -~ 1+ & (cardState.order) %~ interchange i (i-1) - _ -> error "impossible"-handleEvent s _ = continue s+ _ -> error "impossible"+handleEvent gs _ _ = continue gs -next :: State -> EventM Name (Next State)-next s- | s ^. index + 1 < length (s ^. cards) = continue . updateState $ s & index +~ 1- | otherwise = halt s+next :: GlobalState -> CS -> EventM Name (Next GlobalState)+next gs s+ | s ^. index + 1 < length (s ^. cards) = continue . updateCS gs . straightenState $ s & index +~ 1+ | otherwise = halt' gs -previous :: State -> EventM Name (Next State)-previous s | s ^. index > 0 = continue . updateState $ s & index -~ 1- | otherwise = continue s+previous :: GlobalState -> CS -> EventM Name (Next GlobalState)+previous gs s | s ^. index > 0 = continue . updateCS gs . straightenState $ s & index -~ 1+ | otherwise = continue gs -updateState :: State -> State-updateState s =+straightenState :: CS -> CS+straightenState s = let card = (s ^. cards) !! (s ^. index) in s & currentCard .~ card & cardState .~ defaultCardState card@@ -485,75 +408,3 @@ let vali = kvs M.! i valj = kvs M.! j in M.insert j vali (M.insert i valj kvs)--------------------------------------------------------------------------- Attributes ---------------------------------------------------------------------------titleAttr :: AttrName-titleAttr = attrName "title"--textboxAttr :: AttrName-textboxAttr = attrName "textbox"--highlightedChoiceAttr :: AttrName-highlightedChoiceAttr = attrName "highlighted choice"--incorrectChoiceAttr :: AttrName-incorrectChoiceAttr = attrName "incorrect choice"--correctChoiceAttr :: AttrName-correctChoiceAttr = attrName "correct choice"--highlightedOptAttr :: AttrName-highlightedOptAttr = attrName "highlighted option"--selectedOptAttr :: AttrName-selectedOptAttr = attrName "selected option"--correctOptAttr :: AttrName-correctOptAttr = attrName "correct option"--incorrectOptAttr :: AttrName-incorrectOptAttr = attrName "incorrect option"--gapAttr :: AttrName-gapAttr = attrName "gap"--incorrectGapAttr :: AttrName-incorrectGapAttr = attrName "incorrect gap"--correctGapAttr :: AttrName-correctGapAttr = attrName "correct gap"--highlightedElementAttr :: AttrName-highlightedElementAttr = attrName "highlighted element"--grabbedElementAttr :: AttrName-grabbedElementAttr = attrName "grabbed element"--correctElementAttr :: AttrName-correctElementAttr = attrName "correct element"--incorrectElementAttr :: AttrName-incorrectElementAttr = attrName "incorrect element"--theMap :: AttrMap-theMap = attrMap V.defAttr- [ (titleAttr, fg V.yellow)- , (textboxAttr, V.defAttr)- , (highlightedChoiceAttr, fg V.yellow)- , (incorrectChoiceAttr, fg V.red)- , (correctChoiceAttr, fg V.green)- , (incorrectGapAttr, fg V.red `V.withStyle` V.underline)- , (correctGapAttr, fg V.green `V.withStyle` V.underline)- , (highlightedOptAttr, fg V.yellow)- , (selectedOptAttr, fg V.blue)- , (incorrectOptAttr, fg V.red)- , (correctOptAttr, fg V.green)- , (highlightedElementAttr, fg V.yellow)- , (grabbedElementAttr, fg V.blue)- , (correctElementAttr, fg V.green)- , (incorrectElementAttr, fg V.red)- , (gapAttr, V.defAttr `V.withStyle` V.underline)- ]
src/UI/FileBrowser.hs view
@@ -1,7 +1,7 @@ {-# OPTIONS_GHC -Wall #-}-{-# LANGUAGE OverloadedStrings, TemplateHaskell, TupleSections #-}+{-# LANGUAGE OverloadedStrings, FlexibleInstances #-} -module UI.FileBrowser (runFileBrowserUI) where+module UI.FileBrowser (State, drawUI, handleEvent, theMap) where import Brick import Brick.Widgets.Border@@ -12,35 +12,16 @@ import Control.Monad.IO.Class import Lens.Micro.Platform import Parser-import Types+import States+import StateManagement+import Recents+import Runners+import UI.BrickHelpers+import qualified UI.Attributes as A import qualified Graphics.Vty as V -type Event = ()-type Name = ()-data State = State- { _fb :: FileBrowser Name- , _exception :: Maybe String- , _cards :: [Card]- , _filePath :: Maybe FilePath- , _showHidden :: Bool- }--makeLenses ''State--app :: App State Event Name-app = App - { appDraw = drawUI- , appChooseCursor = neverShowCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }--errorAttr :: AttrName-errorAttr = "error"- theMap :: AttrMap-theMap = attrMap V.defAttr+theMap = applyAttrMappings [ (listSelectedFocusedAttr, V.black `on` V.yellow) , (fileBrowserCurrentDirectoryAttr, V.white `on` V.blue) , (fileBrowserSelectionInfoAttr, V.white `on` V.blue)@@ -51,11 +32,10 @@ , (fileBrowserSymbolicLinkAttr, fg V.cyan) , (fileBrowserUnixSocketAttr, fg V.red) , (fileBrowserSelectedAttr, V.white `on` V.magenta)- , (errorAttr, fg V.red)- ]+ ] A.theMap -drawUI :: State -> [Widget Name]-drawUI State{_fb=b, _exception=exc} = [center $ ui <=> help]+drawUI :: FBS -> [Widget Name]+drawUI FBS{_fb=b, _exception'=exc} = [drawException exc, center $ ui <=> help] where ui = hCenter $ vLimit 15 $@@ -67,54 +47,42 @@ , hCenter $ txt "/: search, Ctrl-C or Esc: cancel search" , hCenter $ txt "Enter: change directory or select file" , hCenter $ txt "Esc: quit"- , case exc of- Nothing -> emptyWidget- Just e -> hCenter $ withDefAttr errorAttr $- str e ] -handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s@State{_fb=b} (VtyEvent ev) =- case ev of+handleEvent :: GlobalState -> FBS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs s@FBS{_fb=b, _exception'=excep} (VtyEvent ev) =+ let update = updateFBS gs+ continue' = continue . update+ halt' = continue . popState in+ case (excep, ev) of+ (Just _, _) -> continue' $ s & exception' .~ Nothing+ (_, e) -> case e of V.EvKey V.KEsc [] | not (fileBrowserIsSearching b) ->- halt s+ halt' gs V.EvKey (V.KChar 'c') [V.MCtrl] | not (fileBrowserIsSearching b) ->- halt s+ halt' gs V.EvKey (V.KChar 'h') [] | not (fileBrowserIsSearching b) -> let s' = s & showHidden %~ not in- continue $ s' & fb .~ setFileBrowserEntryFilter (Just (entryFilter (s' ^. showHidden))) b+ continue' $ s' & fb .~ setFileBrowserEntryFilter (Just (entryFilter (s' ^. showHidden))) b _ -> do b' <- handleFileBrowserEvent ev b let s' = s & fb .~ b'- -- If the browser has a selected file after handling the- -- event (because the user pressed Enter), shut down. case ev of V.EvKey V.KEnter [] -> case fileBrowserSelection b' of- [] -> continue s'+ [] -> continue' s' [fileInfo] -> do let fp = fileInfoFilePath fileInfo fileOrExc <- liftIO (try (readFile fp) :: IO (Either IOError String)) case fileOrExc of- Left exc -> continue (s' & exception ?~ displayException exc)+ Left exc -> continue' (s' & exception' ?~ displayException exc) Right file -> case parseCards file of- Left parseError -> continue (s & exception ?~ show parseError)- Right result -> halt (s' & cards .~ result & filePath ?~ fp)- _ -> halt s'-- _ -> continue s'-handleEvent s _ = continue s--runFileBrowserUI :: IO (Maybe ([Card], FilePath))-runFileBrowserUI = do- browser <- newFileBrowser selectNonDirectories () Nothing- let filteredBrowser = setFileBrowserEntryFilter (Just (entryFilter False)) browser- s <- defaultMain app (State filteredBrowser Nothing [] Nothing False)- let mfp = s ^. filePath- return $ fmap (s ^. cards,) mfp+ Left parseError -> continue' (s & exception' ?~ errorBundlePretty parseError)+ -- Right result -> halt' (s' & parsedCards .~ result & filePath ?~ fp)+ Right result -> continue =<< liftIO (do+ addRecent fp+ let gs' = update s'+ (gs' `moveToState`) <$> cardsWithOptionsState (update s') result)+ _ -> halt' gs -entryFilter :: Bool -> FileInfo -> Bool-entryFilter acceptHidden info = fileExtensionMatch "txt" info && (acceptHidden || - case fileInfoFilename info of- ".." -> True- '.' : _ -> False- _ -> True)+ _ -> continue' s'+handleEvent gs _ _ = continue gs
src/UI/Info.hs view
@@ -1,24 +1,15 @@-module UI.Info (runInfoUI) where+module UI.Info (State, drawUI, handleEvent, theMap) where import Brick import Brick.Widgets.Border import Brick.Widgets.Border.Style import Brick.Widgets.Center-import Control.Monad (void)+import States+import StateManagement import qualified Graphics.Vty as V -type Event = ()-type Name = ()-type State = ()--app :: App State Event Name-app = App - { appDraw = (:[]) . const ui- , appChooseCursor = neverShowCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }+drawUI :: IS -> [Widget Name]+drawUI = (:[]) . const ui ui :: Widget Name ui =@@ -31,18 +22,21 @@ hBorder <=> drawInfo -handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s (VtyEvent e) =+handleEvent :: GlobalState -> IS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs s (VtyEvent e) =+ let update = updateIS gs+ continue' = continue . update+ halt' = continue . popState in case e of- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s- V.EvKey V.KEsc [] -> halt s- V.EvKey V.KEnter [] -> halt s- V.EvKey V.KDown [] -> vScrollBy (viewportScroll ()) 1 >> continue s- V.EvKey (V.KChar 'j') [] -> vScrollBy (viewportScroll ()) 1 >> continue s- V.EvKey V.KUp [] -> vScrollBy (viewportScroll ()) (-1) >> continue s- V.EvKey (V.KChar 'k') [] -> vScrollBy (viewportScroll ()) (-1) >> continue s- _ -> continue s-handleEvent s _ = continue s+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt' gs+ V.EvKey V.KEsc [] -> halt' gs+ V.EvKey V.KEnter [] -> halt' gs+ V.EvKey V.KDown [] -> vScrollBy (viewportScroll Ordinary) 1 >> continue' s+ V.EvKey (V.KChar 'j') [] -> vScrollBy (viewportScroll Ordinary) 1 >> continue' s+ V.EvKey V.KUp [] -> vScrollBy (viewportScroll Ordinary) (-1) >> continue' s+ V.EvKey (V.KChar 'k') [] -> vScrollBy (viewportScroll Ordinary) (-1) >> continue' s+ _ -> continue' s+handleEvent gs _ _ = continue gs titleAttr :: AttrName titleAttr = attrName "title"@@ -55,11 +49,23 @@ drawInfo = padLeftRight 1 $ vLimitPercent 60 $- viewport () Vertical (strWrap info)--runInfoUI :: IO ()-runInfoUI = void $ defaultMain app ()+ viewport Ordinary Vertical (strWrap info) info :: String-info = - "Hascard is a text-based user interface for reviewing notes using 'flashcards'. Cards are written in markdown-like syntax; for more info see the README file. Use the --help flag for information on the command line options.\n\nControls:\n * Use arrows or the j and k keys for menu navigation\n * Enter confirms a selection, flips a card or continues to the next card\n * Use TAB or the arrow keys for navigating gaps in open questions\n * Use the c key for confirming reorder questions or multiple choice questions with more than 1 possible answer\n * Use F1 to show the answers of a open question.\n * Use CTRL+Left and CTRL+Right to move to previous and next cards without having to answer them"+info = unlines+ [ "Hascard is a text-based user interface for reviewing notes using 'flashcards'. Cards are written in markdown-like syntax; for more info see the README file. Use the --help flag for information on the command line options."+ , ""+ , "Controls:"+ , " * Use arrows or the j and k keys for menu navigation"+ , ""+ , " * Press the s key to toggle shuffling inside the deck selector menu"+ , ""+ , " * Enter confirms a selection, flips a card or continues to the next card"+ , ""+ , " * Use TAB or the arrow keys for navigating gaps in open questions"+ , ""+ , " * Use the c key for confirming reorder questions or multiple choice questions with more than 1 possible answer"+ , ""+ , " * Use F1 to show the answers of a open question"+ , ""+ , " * Use CTRL+Left and CTRL+Right to move to previous and next cards without having to answer them"]
src/UI/MainMenu.hs view
@@ -1,49 +1,31 @@-{-# LANGUAGE TemplateHaskell #-}-module UI.MainMenu (runMainMenuUI) where+module UI.MainMenu (State (..), drawUI, handleEvent, theMap) where import Brick import Brick.Widgets.Border import Brick.Widgets.Border.Style import Brick.Widgets.Center-import Data.Functor (($>))+import Control.Monad.IO.Class import Lens.Micro.Platform-import Types (GlobalState)+import Runners+import Settings+import States+import StateManagement+import UI.Attributes import UI.BrickHelpers-import UI.CardSelector-import UI.Info-import UI.Settings-import qualified Data.Vector as Vec import qualified Graphics.Vty as V import qualified Brick.Widgets.List as L -type Event = ()-type Name = ()-data State = State - { _l :: L.List Name String- , _gs :: GlobalState }--makeLenses ''State--app :: App State Event Name-app = App - { appDraw = drawUI- , appChooseCursor = neverShowCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }- title :: Widget Name title = withAttr titleAttr $ str "┬ ┬┌─┐┌─┐┌─┐┌─┐┬─┐┌┬┐" <=> str "├─┤├─┤└─┐│ ├─┤├┬┘ ││" <=> str "┴ ┴┴ ┴└─┘└─┘┴ ┴┴└──┴┘" -drawUI :: State -> [Widget Name]+drawUI :: MMS -> [Widget Name] drawUI s = [ drawMenu s ] -drawMenu :: State -> Widget Name+drawMenu :: MMS -> Widget Name drawMenu s = joinBorders $ center $ @@ -54,7 +36,7 @@ hBorder <=> drawList s -drawList :: State -> Widget Name+drawList :: MMS -> Widget Name drawList s = vLimit 4 $ L.renderList drawListElement True (s^.l) @@ -62,59 +44,19 @@ drawListElement selected = hCenteredStrWrapWithAttr attr where attr = if selected then withAttr selectedAttr else id -titleAttr :: AttrName-titleAttr = attrName "title"--selectedAttr :: AttrName-selectedAttr = attrName "selected"--theMap :: AttrMap-theMap = attrMap V.defAttr- [ (L.listAttr, V.defAttr)- , (selectedAttr, fg V.white `V.withStyle` V.underline)- , (titleAttr, fg V.yellow) ]--handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s (VtyEvent e) =+handleEvent :: GlobalState -> MMS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs s (VtyEvent e) =+ let update = updateMMS gs in case e of- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s- V.EvKey V.KEsc [] -> halt s+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt gs+ V.EvKey V.KEsc [] -> halt gs V.EvKey V.KEnter [] -> case L.listSelected (s^.l) of- Just 0 -> suspendAndResume $ runCardSelectorUI (s^.gs)$> s- Just 1 -> suspendAndResume $ runInfoUI $> s- Just 2 -> suspendAndResume $ runSettingsUI $> s- Just 3 -> halt s+ Just 0 -> continue =<< (gs `goToState`) <$> liftIO cardSelectorState+ Just 1 -> continue $ gs `goToState` infoState+ Just 2 -> continue =<< (gs `goToState`) <$> liftIO settingsState + Just 3 -> halt gs _ -> undefined - ev -> continue . flip (l .~) s =<< L.handleListEventVi L.handleListEvent ev (s^.l)-handleEvent l _ = continue l--runMainMenuUI :: GlobalState -> IO ()-runMainMenuUI gs = do- let options = Vec.fromList [ "Select"- , "Info"- , "Settings"- , "Quit" ]-- let initialState = State (L.list () options 1) gs- _ <- defaultMain app initialState- return ()---- _ _ _ --- | | | | | |--- | |__| | __ _ ___ ___ __ _ _ __ __| |--- | __ |/ _` / __|/ __/ _` | '__/ _` |--- | | | | (_| \__ \ (_| (_| | | | (_| |--- |_| |_|\__,_|___/\___\__,_|_| \__,_|- --- _ _ --- | | | |--- | |__ __ _ ___ ___ __ _ _ __ __| |--- | '_ \ / _` / __|/ __/ _` | '__/ _` |--- | | | | (_| \__ \ (_| (_| | | | (_| |--- |_| |_|\__,_|___/\___\__,_|_| \__,_|- --- ┬ ┬┌─┐┌─┐┌─┐┌─┐┬─┐┌┬┐--- ├─┤├─┤└─┐│ ├─┤├┬┘ ││--- ┴ ┴┴ ┴└─┘└─┘┴ ┴┴└──┴┘+ ev -> continue . update . flip (l .~) s =<< L.handleListEventVi L.handleListEvent ev (s^.l)+handleEvent gs _ _ = continue gs
src/UI/Settings.hs view
@@ -1,36 +1,23 @@-module UI.Settings (runSettingsUI, getShowHints, getShowControls, getUseEscapeCode) where+module UI.Settings (State, drawUI, handleEvent, theMap) where +import UI.Attributes import Brick hiding (mergeWithDefault)+import Brick.Focus+import Brick.Forms import Brick.Widgets.Border import Brick.Widgets.Border.Style import Brick.Widgets.Center-import Control.Monad (void)-import Data.Functor (($>))-import Data.Map.Strict (Map, (!))-import System.FilePath ((</>))-import System.Environment (lookupEnv)-import Text.Read (readMaybe)-import UI.BrickHelpers-import qualified Data.Map.Strict as M+import Control.Monad.IO.Class+import States+import StateManagement+import Settings import qualified Graphics.Vty as V-import qualified System.Directory as D -type Event = ()-type Name = ()-type Settings = Map Int Bool-type State = (Int, Settings)--app :: App State Event Name-app = App - { appDraw = (:[]) . ui- , appChooseCursor = neverShowCursor- , appHandleEvent = handleEvent- , appStartEvent = return- , appAttrMap = const theMap- }+drawUI :: SS -> [Widget Name]+drawUI = (:[]) . ui -ui :: State -> Widget Name-ui s =+ui :: SS -> Widget Name+ui f = joinBorders $ center $ withBorderStyle unicodeRounded $@@ -40,104 +27,20 @@ hCenter (withAttr titleAttr (str "Settings")) <=> hBorder <=> padLeftRight 1- (drawSettings s)+ (renderForm f) -handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent s@(i, settings) (VtyEvent e) =+handleEvent :: GlobalState -> SS -> BrickEvent Name Event -> EventM Name (Next GlobalState)+handleEvent gs form ev@(VtyEvent e) =+ let update = updateSS gs+ continue' = continue . update+ halt' global = continue (popState global) <* liftIO (setSettings (formState form)) in case e of- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s- V.EvKey V.KEsc [] -> halt s- V.EvKey V.KEnter [] -> continue (i, settings')- where settings' = M.adjust not i settings- V.EvKey V.KUp [] -> continue (max 0 (i-1), settings)- V.EvKey (V.KChar 'k') [] -> continue (max 0 (i-1), settings)- V.EvKey V.KDown [] -> continue (min (M.size settings-1) (i+1), settings)- V.EvKey (V.KChar 'j') [] -> continue (min (M.size settings-1) (i+1), settings)- _ -> continue s-handleEvent s _ = continue s--titleAttr :: AttrName-titleAttr = attrName "title"--selectedAttr :: AttrName-selectedAttr = attrName "selected"--theMap :: AttrMap-theMap = attrMap V.defAttr- [ (titleAttr, fg V.yellow),- (selectedAttr, V.defAttr `V.withStyle` V.underline) ]--drawSettings :: State -> Widget Name-drawSettings s = vBox $ map (drawSetting s) (zip [0..] descriptions)- where descriptions = map (++": ") - [ "Draw hints using underscores for definition cards"- , "Show controls at the bottom of screen"- , "Use the '-n \\e[5 q' escape code to change the cursor to a blinking line on start" ]--drawSetting :: State -> (Int, String) -> Widget Name-drawSetting (selected, settings) (i, text) =- strWrap text <+> str " " <+> word- where word = if settings ! i then underline (str "Yes") else underline (str "No") <+> str " "- underline = if i == selected then withAttr selectedAttr else id--runSettingsUI :: IO ()-runSettingsUI = do- currentSettings <- getSettings- (_, newSettings) <- defaultMain app (0, currentSettings)- setSettings newSettings--getSettings :: IO Settings-getSettings = do- sf <- getSettingsFile- exists <- D.doesFileExist sf- if exists - then do- maybeSettings <- parseSettings <$> readFile sf- flip (maybe (return defaultSettings)) maybeSettings $ \settings ->- if M.size settings == M.size defaultSettings- then return settings- else let settings' = settings `mergeWithDefault` defaultSettings in- setSettings settings' $> settings'-- else return defaultSettings--mergeWithDefault :: Settings -> Settings -> Settings-mergeWithDefault = flip M.union--getShowHints :: IO Bool-getShowHints = do- settings <- getSettings- return $ settings ! 0 --getShowControls :: IO Bool-getShowControls = do- settings <- getSettings- return $ settings ! 1--getUseEscapeCode :: IO Bool-getUseEscapeCode = do- settings <- getSettings- return $ settings ! 2--parseSettings :: String -> Maybe Settings-parseSettings = readMaybe--getSettingsFile :: IO FilePath-getSettingsFile = do- maybeSnap <- lookupEnv "SNAP_USER_DATA"- xdg <- D.getXdgDirectory D.XdgConfig "hascard"-- let dir = case maybeSnap of- Just path | not (null path) -> path- | otherwise -> xdg- Nothing -> xdg- D.createDirectoryIfMissing True dir- return (dir </> "settings")--defaultSettings :: Settings-defaultSettings = M.fromList [(0, False), (1, True), (2, False)]+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt' gs+ V.EvKey V.KEsc [] -> halt' gs+ V.EvKey V.KDown [] -> continue' $ form { formFocus = focusNext (formFocus form) }+ V.EvKey (V.KChar 'j') [] -> continue' $ form { formFocus = focusNext (formFocus form) }+ V.EvKey V.KUp [] -> continue' $ form { formFocus = focusPrev (formFocus form) }+ V.EvKey (V.KChar 'k') [] -> continue' $ form { formFocus = focusPrev (formFocus form) }+ _ -> continue' =<< handleFormEvent ev form -setSettings :: Settings -> IO ()-setSettings settings = do- sf <- getSettingsFile- writeFile sf (show settings)+handleEvent gs _ _ = continue gs