1
1
mirror of https://github.com/Yvee1/hascard.git synced 2024-11-25 09:54:43 +03:00

Add ordering type question, parsing and display

This commit is contained in:
Steven van den Broek 2020-07-25 13:16:20 +02:00
parent 7a23a760d3
commit a0887b039f
4 changed files with 218 additions and 89 deletions

View File

@ -22,4 +22,10 @@ Explanation or definition of this word, or the answer to the question.
# Fill in the gaps # Fill in the gaps
The symbol € is for the currency named _Euro_, and is used in the _EU|European Union_. The symbol € is for the currency named _Euro_, and is used in the _EU|European Union_.
--- ---
# Order the letters in alphabetical order
4. u
1. l
2. p
3. s

View File

@ -13,6 +13,7 @@ parseCards = parse pCards "failed when parsing cards"
pCards = pCard `sepEndBy1` seperator pCards = pCard `sepEndBy1` seperator
pCard = uncurry3 MultipleChoice<$> try pMultChoice pCard = uncurry3 MultipleChoice<$> try pMultChoice
<|> uncurry MultipleAnswer <$> try pMultAnswer <|> uncurry MultipleAnswer <$> try pMultAnswer
<|> uncurry Reorder <$> try pReorder
<|> uncurry OpenQuestion <$> try pOpen <|> uncurry OpenQuestion <$> try pOpen
<|> uncurry Definition <$> pDef <|> uncurry Definition <$> pDef
@ -32,7 +33,7 @@ pMultChoice = do
pChoice = do pChoice = do
kind <- oneOf "*-" kind <- oneOf "*-"
space space
text <- manyTill anyChar $ lookAhead (try (try choicePrefix <|> seperator <|> (eof >> return []))) text <- manyTill anyChar $ lookAhead (try (try choicePrefix <|> seperator <|> eof'))
return (kind, text) return (kind, text)
choicePrefix = string "- " choicePrefix = string "- "
@ -48,9 +49,25 @@ pOption = do
char '[' char '['
kind <- oneOf "*x " kind <- oneOf "*x "
string "] " string "] "
text <- manyTill anyChar $ lookAhead (try (seperator <|> string "[" <|> (eof >> return []))) text <- manyTill anyChar $ lookAhead (try (seperator <|> string "[" <|> eof'))
return $ makeOption kind text return $ makeOption kind text
pReorder = do
header <- pHeader
many eol
elements <- pReorderElement `sepBy1` lookAhead (try pReorderPrefix)
return (header, NE.fromList elements)
pReorderElement = do
int <- pReorderPrefix
text <- manyTill anyChar $ lookAhead (try (try seperator <|> try pReorderPrefix <|> eof'))
return (read int, text)
pReorderPrefix = do
int <- many1 digit
string ". "
return int
pOpen = do pOpen = do
header <- pHeader header <- pHeader
many eol many eol
@ -81,13 +98,13 @@ gappedSpecialChars = seperator
<|> string "_" <|> string "_"
pNormal = do pNormal = do
text <- manyTill (noneOf "_") $ lookAhead $ try $ gappedSpecialChars <|> (eof >> return []) text <- manyTill (noneOf "_") $ lookAhead $ try $ gappedSpecialChars <|> eof'
return (Normal text) return (Normal text)
pDef = do pDef = do
header <- pHeader header <- pHeader
many eol many eol
descr <- manyTill chars $ lookAhead (try (seperator <|> (eof >> return []))) descr <- manyTill chars $ lookAhead $ try $ seperator <|> eof'
return (header, descr) return (header, descr)
eol = try (string "\n\r") eol = try (string "\n\r")
@ -96,6 +113,8 @@ eol = try (string "\n\r")
<|> string "\r" <|> string "\r"
<?> "end of line" <?> "end of line"
eof' = eof >> return [] <?> "end of file"
seperator = do seperator = do
sep <- string "---" sep <- string "---"
many eol many eol

View File

@ -15,13 +15,16 @@ makeLenses ''GlobalState
data Card = Definition String String data Card = Definition String String
| OpenQuestion String Perforated | OpenQuestion String Perforated
| MultipleChoice { | MultipleChoice {
mcQuestion :: String, question :: String,
mcCorrect :: CorrectOption, correct :: CorrectOption,
mcIncorrects :: [IncorrectOption]} incorrects :: [IncorrectOption]}
-- | MultipleAnswer String (NE.NonEmpty Answer)
| MultipleAnswer { | MultipleAnswer {
maQuestion :: String, question :: String,
maOptions :: NonEmpty Option } options :: NonEmpty Option }
| Reorder {
question :: String,
elements :: NonEmpty (Int, String)
}
deriving Show deriving Show

View File

@ -28,22 +28,29 @@ data CardState =
{ _flipped :: Bool } { _flipped :: Bool }
| MultipleChoiceState | MultipleChoiceState
{ _highlighted :: Int { _highlighted :: Int
, _nChoices :: Int , _number :: Int
, _tried :: Map Int Bool -- indices of tried choices , _tried :: Map Int Bool -- indices of tried choices
} }
| MultipleAnswerState | MultipleAnswerState
{ _highlighted :: Int { _highlighted :: Int
, _selected :: Map Int Bool , _selected :: Map Int Bool
, _nChoices :: Int , _number :: Int
, _entered :: Bool , _entered :: Bool
} }
| OpenQuestionState | OpenQuestionState
{ _gapInput :: Map Int String { _gapInput :: Map Int String
, _highlighted :: Int , _highlighted :: Int
, _nGaps :: Int , _number :: Int
, _entered :: Bool , _entered :: Bool
, _correctGaps :: Map Int Bool , _correctGaps :: Map Int Bool
} }
| ReorderState
{ _highlighted :: Int
, _grabbed :: Bool
, _order :: Map Int (Int, String)
, _entered :: Bool
, _number :: Int
}
data State = State data State = State
{ _cards :: [Card] -- list of flashcards { _cards :: [Card] -- list of flashcards
@ -63,19 +70,25 @@ defaultCardState :: Card -> CardState
defaultCardState Definition{} = DefinitionState { _flipped = False } defaultCardState Definition{} = DefinitionState { _flipped = False }
defaultCardState (MultipleChoice _ _ ics) = MultipleChoiceState defaultCardState (MultipleChoice _ _ ics) = MultipleChoiceState
{ _highlighted = 0 { _highlighted = 0
, _nChoices = length ics + 1 , _number = length ics + 1
, _tried = M.fromList [(i, False) | i <- [0..length ics]] } , _tried = M.fromList [(i, False) | i <- [0..length ics]] }
defaultCardState (OpenQuestion _ perforated) = OpenQuestionState defaultCardState (OpenQuestion _ perforated) = OpenQuestionState
{ _gapInput = M.empty { _gapInput = M.empty
, _highlighted = 0 , _highlighted = 0
, _nGaps = nGapsInPerforated perforated , _number = nGapsInPerforated perforated
, _entered = False , _entered = False
, _correctGaps = M.fromList [(i, False) | i <- [0..nGapsInPerforated perforated - 1]] } , _correctGaps = M.fromList [(i, False) | i <- [0..nGapsInPerforated perforated - 1]] }
defaultCardState (MultipleAnswer _ answers) = MultipleAnswerState defaultCardState (MultipleAnswer _ answers) = MultipleAnswerState
{ _highlighted = 0 { _highlighted = 0
, _selected = M.fromList [(i, False) | i <- [0..NE.length answers-1]] , _selected = M.fromList [(i, False) | i <- [0..NE.length answers-1]]
, _entered = False , _entered = False
, _nChoices = NE.length answers } , _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 State Event Name
app = App app = App
@ -86,6 +99,24 @@ app = App
, appAttrMap = const theMap , 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 :: State -> [Widget Name]
drawUI s = [drawCardUI s <=> drawInfo s] drawUI s = [drawCardUI s <=> drawInfo s]
@ -93,13 +124,35 @@ drawInfo :: State -> Widget Name
drawInfo s = if not (s ^. showControls) then emptyWidget else drawInfo s = if not (s ^. showControls) then emptyWidget else
strWrap . ("ESC: quit" <>) $ case s ^. cardState of strWrap . ("ESC: quit" <>) $ case s ^. cardState of
DefinitionState {} -> ", ENTER: flip card / continue" DefinitionState {} -> ", ENTER: flip card / continue"
MultipleChoiceState {} -> ", ENTER: confirm answer / continue" MultipleChoiceState {} -> ", ENTER: submit answer / continue"
MultipleAnswerState {} -> ", ENTER: select / continue, c: confirm selection" MultipleAnswerState {} -> ", ENTER: select / continue, c: submit selection"
OpenQuestionState {} -> ", LEFT/RIGHT/TAB: navigate gaps, ENTER: confirm answer / continue" OpenQuestionState {} -> ", LEFT/RIGHT/TAB: navigate gaps, ENTER: submit answer / continue"
ReorderState {} -> ", ENTER: grab, c: submit answer"
drawCardBox :: Widget Name -> Widget Name
drawCardBox w = C.center $
withBorderStyle BS.unicodeRounded $
B.border $
withAttr textboxAttr $
hLimitPercent 60 w
drawProgress :: State -> Widget Name drawProgress :: State -> Widget Name
drawProgress s = C.hCenter $ str (show (s^.index + 1) ++ "/" ++ show (s^.nCards)) drawProgress s = C.hCenter $ str (show (s^.index + 1) ++ "/" ++ show (s^.nCards))
drawCardUI :: State -> 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 " ")
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 " ")
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 " ")
drawHeader :: String -> Widget Name drawHeader :: String -> Widget Name
drawHeader title = withAttr titleAttr $ drawHeader title = withAttr titleAttr $
padLeftRight 1 $ padLeftRight 1 $
@ -118,29 +171,6 @@ drawDescr descr =
where where
descr' = dropWhileEnd isSpace' descr descr' = dropWhileEnd isSpace' descr
listMultipleChoice :: CorrectOption -> [IncorrectOption] -> [String]
listMultipleChoice c = reverse . listMultipleChoice' [] 0 c
where listMultipleChoice' opts i (CorrectOption j cStr) [] =
if i == j
then cStr : opts
else opts
listMultipleChoice' opts i c'@(CorrectOption j cStr) ics@(IncorrectOption icStr : ics') =
if i == j
then listMultipleChoice' (cStr : opts) (i+1) c' ics
else listMultipleChoice' (icStr : opts) (i+1) c' ics'
drawCardUI :: State -> 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 " ")
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 " ")
MultipleAnswer question options -> drawHeader question <=> B.hBorder <=> padRight (Pad p) (drawOptions s options <=> str " ")
drawDef :: State -> String -> Widget Name drawDef :: State -> String -> Widget Name
drawDef s def = if s ^. showHints then drawHintedDef s def else drawNormalDef s def drawDef s def = if s ^. showHints then drawHintedDef s def else drawNormalDef s def
@ -160,6 +190,17 @@ drawNormalDef s def = case s ^. cardState of
render . vBox $ [str " " | _ <- wrapTextToLines wrapSettings w (pack def')] render . vBox $ [str " " | _ <- wrapTextToLines wrapSettings w (pack def')]
_ -> error "impossible: " _ -> error "impossible: "
listMultipleChoice :: CorrectOption -> [IncorrectOption] -> [String]
listMultipleChoice c = reverse . listMultipleChoice' [] 0 c
where listMultipleChoice' opts i (CorrectOption j cStr) [] =
if i == j
then cStr : opts
else opts
listMultipleChoice' opts i c'@(CorrectOption j cStr) ics@(IncorrectOption icStr : ics') =
if i == j
then listMultipleChoice' (cStr : opts) (i+1) c' ics
else listMultipleChoice' (icStr : opts) (i+1) c' ics'
drawChoices :: State -> [String] -> Widget Name drawChoices :: State -> [String] -> Widget Name
drawChoices s options = case (s ^. cardState, s ^. currentCard) of drawChoices s options = case (s ^. cardState, s ^. currentCard) of
(MultipleChoiceState {_highlighted=i, _tried=kvs}, MultipleChoice _ (CorrectOption k _) _) -> vBox formattedOptions (MultipleChoiceState {_highlighted=i, _tried=kvs}, MultipleChoice _ (CorrectOption k _) _) -> vBox formattedOptions
@ -256,12 +297,29 @@ wrapStringWithPadding padding w s
ts' = ts & _last %~ (`T.append` postfix) in ts' = ts & _last %~ (`T.append` postfix) in
(map txt (filter (/=T.empty) ts'), T.length (last ts'), False) (map txt (filter (/=T.empty) ts'), T.length (last ts'), False)
drawCardBox :: Widget Name -> Widget Name drawReorder :: State -> NonEmpty (Int, String) -> Widget Name
drawCardBox w = C.center $ drawReorder s elements = case (s ^. cardState, s ^. currentCard) of
withBorderStyle BS.unicodeRounded $ (ReorderState {_highlighted=j, _grabbed=g, _order=kvs, _number=n, _entered=submitted}, Reorder _ _) ->
B.border $ vBox . flip map (map (\i -> (i, kvs M.! i)) [0..n-1]) $
withAttr textboxAttr $ \(i, (k, text)) ->
hLimitPercent 60 w let color = case (i == j, g) of
(True, True ) -> withAttr grabbedElementAttr
(True, False) -> withAttr highlightedElementAttr
_ -> id
number =
case (submitted, i+1 == k) of
(False, _) -> str (show (i+1) <> ". ")
(True, False) -> withAttr incorrectElementAttr (str (show k <> ". "))
(True, True ) -> withAttr correctElementAttr (str (show k <> ". "))
in
number <+> color (drawDescr text)
_ -> error "cardstate mismatch"
----------------------------------------------------
---------------------- Events ----------------------
----------------------------------------------------
handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State) handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)
handleEvent s (VtyEvent e) = case e of handleEvent s (VtyEvent e) = case e of
@ -279,7 +337,7 @@ handleEvent s (VtyEvent e) = case e of
else continue $ s & cardState.flipped %~ not else continue $ s & cardState.flipped %~ not
_ -> continue s _ -> continue s
(MultipleChoiceState {_highlighted = i, _nChoices = n, _tried = kvs}, MultipleChoice _ (CorrectOption j _) _) -> (MultipleChoiceState {_highlighted = i, _number = n, _tried = kvs}, MultipleChoice _ (CorrectOption j _) _) ->
case ev of case ev of
V.EvKey V.KUp [] -> continue up V.EvKey V.KUp [] -> continue up
V.EvKey (V.KChar 'k') [] -> continue up V.EvKey (V.KChar 'k') [] -> continue up
@ -295,15 +353,15 @@ handleEvent s (VtyEvent e) = case e of
where frozen = M.findWithDefault False j kvs where frozen = M.findWithDefault False j kvs
down = if i < n - 1 && not frozen down = if i < n && not frozen
then s & (cardState.highlighted) +~ 1 then s & (cardState.highlighted) +~ 1
else s else s
up = if i > 0 && not frozen up = if i > 1 && not frozen
then s & (cardState.highlighted) -~ 1 then s & (cardState.highlighted) -~ 1
else s else s
(MultipleAnswerState {_highlighted = i, _nChoices = n, _entered = submitted}, MultipleAnswer {}) -> (MultipleAnswerState {_highlighted = i, _number = n, _entered = submitted}, MultipleAnswer {}) ->
case ev of case ev of
V.EvKey V.KUp [] -> continue up V.EvKey V.KUp [] -> continue up
V.EvKey (V.KChar 'k') [] -> continue up V.EvKey (V.KChar 'k') [] -> continue up
@ -330,7 +388,7 @@ handleEvent s (VtyEvent e) = case e of
then s & (cardState.highlighted) -~ 1 then s & (cardState.highlighted) -~ 1
else s else s
(OpenQuestionState {_highlighted = i, _nGaps = n, _gapInput = kvs, _correctGaps = cGaps}, OpenQuestion _ perforated) -> (OpenQuestionState {_highlighted = i, _number = n, _gapInput = kvs, _correctGaps = cGaps}, OpenQuestion _ perforated) ->
let correct = M.foldr (&&) True cGaps in let correct = M.foldr (&&) True cGaps in
case ev of case ev of
V.EvKey (V.KChar '\t') [] -> continue $ V.EvKey (V.KChar '\t') [] -> continue $
@ -363,9 +421,69 @@ handleEvent s (VtyEvent e) = case e of
backspace xs = init xs backspace xs = init xs
_ -> continue s _ -> 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.KEnter [] ->
if frozen
then next s
else continue $ s & cardState.grabbed %~ not
_ -> 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)
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" _ -> error "impossible"
handleEvent s _ = continue s handleEvent s _ = continue s
next :: State -> EventM Name (Next State)
next s
| s ^. index + 1 < length (s ^. cards) = continue . updateState $ s & index +~ 1
| otherwise = halt s
previous :: State -> EventM Name (Next State)
previous s | s ^. index > 0 = continue . updateState $ s & index -~ 1
| otherwise = continue s
updateState :: State -> State
updateState s =
let card = (s ^. cards) !! (s ^. index) in s
& currentCard .~ card
& cardState .~ defaultCardState card
interchange :: (Ord a) => a -> a -> Map a b -> Map a b
interchange i j kvs =
let vali = kvs M.! i
valj = kvs M.! j in
M.insert j vali (M.insert i valj kvs)
----------------------------------------------------
-------------------- Attributes --------------------
----------------------------------------------------
titleAttr :: AttrName titleAttr :: AttrName
titleAttr = attrName "title" titleAttr = attrName "title"
@ -393,9 +511,6 @@ correctOptAttr = attrName "correct option"
incorrectOptAttr :: AttrName incorrectOptAttr :: AttrName
incorrectOptAttr = attrName "incorrect option" incorrectOptAttr = attrName "incorrect option"
hiddenAttr :: AttrName
hiddenAttr = attrName "hidden"
gapAttr :: AttrName gapAttr :: AttrName
gapAttr = attrName "gap" gapAttr = attrName "gap"
@ -405,6 +520,18 @@ incorrectGapAttr = attrName "incorrect gap"
correctGapAttr :: AttrName correctGapAttr :: AttrName
correctGapAttr = attrName "correct gap" 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
theMap = attrMap V.defAttr theMap = attrMap V.defAttr
[ (titleAttr, fg V.yellow) [ (titleAttr, fg V.yellow)
@ -418,35 +545,9 @@ theMap = attrMap V.defAttr
, (selectedOptAttr, fg V.blue) , (selectedOptAttr, fg V.blue)
, (incorrectOptAttr, fg V.red) , (incorrectOptAttr, fg V.red)
, (correctOptAttr, fg V.green) , (correctOptAttr, fg V.green)
, (hiddenAttr, fg V.black) , (highlightedElementAttr, fg V.yellow)
, (grabbedElementAttr, fg V.blue)
, (correctElementAttr, fg V.green)
, (incorrectElementAttr, fg V.red)
, (gapAttr, V.defAttr `V.withStyle` V.underline) , (gapAttr, V.defAttr `V.withStyle` V.underline)
] ]
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
next :: State -> EventM Name (Next State)
next s
| s ^. index + 1 < length (s ^. cards) = continue . updateState $ s & index +~ 1
| otherwise = halt s
previous :: State -> EventM Name (Next State)
previous s | s ^. index > 0 = continue . updateState $ s & index -~ 1
| otherwise = continue s
updateState :: State -> State
updateState s =
let card = (s ^. cards) !! (s ^. index) in s
& currentCard .~ card
& cardState .~ defaultCardState card