1
1
mirror of https://github.com/Yvee1/hascard.git synced 2024-11-29 00:03:03 +03:00
hascard/app/Main.hs

161 lines
7.5 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
module Main where
import UI
import Control.Exception (displayException, try)
import Control.Monad (void, when)
import Data.Version (showVersion)
import Data.Void
import Lens.Micro.Platform
import Paths_hascard (version)
import Parser
import Options.Applicative hiding (many)
import System.Directory (makeAbsolute)
import System.FilePath (takeExtension, takeBaseName, takeDirectory)
import System.Process (runCommand)
import System.Random.MWC (createSystemRandom)
import Text.Megaparsec (parse, many, errorBundlePretty, ParseErrorBundle)
import Text.Megaparsec.Char.Lexer (charLiteral)
import qualified Data.Map.Strict as Map (empty)
import qualified System.Directory as D
import qualified Stack
data Command = Command
| Run RunOpts
| Import ImportOpts
data Opts = Opts
{ _optCommand :: Maybe Command
, _optVersion :: Bool
}
data RunOpts = RunOpts
{ _optFile :: Maybe String
, _optSubset :: Int
, _optChunk :: Chunk
, _optShuffle :: Bool
, _optBlankMode :: Bool
}
makeLenses ''Opts
makeLenses ''RunOpts
makeLenses ''ImportOpts
main :: IO ()
main = do
useEscapeCode <- getUseEscapeCode
when useEscapeCode $ void (runCommand "echo -n \\\\e[5 q")
options <- execParser optsWithHelp
case (options ^. optVersion, options ^. optCommand) of
(True, _) -> putStrLn (showVersion version)
(_, Just (Run rOpts)) -> run rOpts
(_, Just (Import iOpts)) -> doImport iOpts
(_, Nothing) ->
do g <- createSystemRandom
let gs = GlobalState {_mwc=g, _states=Map.empty, _stack=Stack.empty, _parameters= defaultParameters }
start Nothing gs
opts :: Parser Opts
opts = Opts
<$> optional (hsubparser
( command "run" (info (Run <$> runOpts) ( progDesc "Run hascard with CLI options"))
<> command "import" (info (Import <$> importOpts) (progDesc "Convert a delimited text file (e.g. exported from Quizlet) to a file compatible with hascard.\
\ The delimiters can be specified via CLI options. By default, terms and definitions are assumed to be separated by tabs, and different cards by new lines.\
\ Either 'Definition' cards are generated (traditional flashcards), or 'Open question' cards (where the answer needs to be typed).\
\ For 'Open question' cards, a delimiter can be specified which separates multiple correct answers."))))
<*> switch (long "version" <> short 'v' <> help "Show version number")
runOpts :: Parser RunOpts
runOpts = RunOpts
<$> optional (argument str (metavar "FILE" <> help "A .txt or .md 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 "blank" <> short 'b' <> help "Disable review mode: do not keep track of which questions were correctly and incorrectly answered")
importOpts :: Parser ImportOpts
importOpts = ImportOpts
<$> argument str (metavar "INPUT" <> help "A delimited text file")
<*> argument str (metavar "DESINATION" <> help "The filename/path to which the output should be saved")
<*> option auto (long "type" <> short 't' <> metavar "'open' or 'def'" <> help "The type of card to which the input is transformed; default: open." <> value Open)
<*> switch (long "reverse" <> short 'r' <> help "Reverse direction of question and answer, i.e. right part becomes the question.")
<*> strOption (long "row-delimiter" <> metavar "delimiter" <> help "The delimiter used to separate different cards; default: \\n." <> value "\n")
<*> strOption (long "term-def-delimiter" <> metavar "delimiter" <> help "The delimiter used to separate terms and definitions; default: \\t." <> value "\t")
<*> optional (strOption (long "def-delimiter" <> metavar "delimiter" <> help "The delimiter used to separate different definitions for the same term; no delimiter is used by default."))
optsWithHelp :: ParserInfo Opts
optsWithHelp = info (opts <**> helper) $
fullDesc <> progDesc "Run the normal application with `hascard`. To run directly on a file, and with CLI options, see `hascard run --help`.\
\ For converting delimited text files, see `hascard import --help`."
<> header "Hascard - a TUI for reviewing notes"
nothingIf :: (a -> Bool) -> a -> Maybe a
nothingIf p a
| p a = Nothing
| otherwise = Just a
mkGlobalState :: RunOpts -> GenIO -> GlobalState
mkGlobalState opts gen =
let ps = Parameters { _pShuffle = opts^.optShuffle, _pSubset = nothingIf (<0) (opts^.optSubset)
, _pChunk = opts^.optChunk, _pReviewMode = not (opts^.optBlankMode), _pOk = False }
in GlobalState {_mwc=gen, _states=Map.empty, _stack=Stack.empty, _parameters=ps }
cleanFilePath :: FilePath -> IO (Either String FilePath)
cleanFilePath fp = case takeExtension fp of
".txt" -> return $ Right fp
".md" -> return $ Right fp
"" -> do existence <- mapM D.doesFileExist [fp <> ".txt", fp <> ".md"]
return $ case existence of
[True, True] -> Left "Both a .txt and .md file of this name exist, and it is unclear which to use. Specify the file extension."
[True, _ ] -> Right $ fp <> ".txt"
[_ , True] -> Right $ fp <> ".md"
_ -> Left $ "No .txt or .md file with the name \""
<> takeBaseName fp
<> "\" in the directory \""
<> takeDirectory fp
<> "\""
_ -> return $ Left "Incorrect file type, provide a .txt file"
run :: RunOpts -> IO ()
run opts = run' (opts ^. optFile)
where
run' Nothing = createSystemRandom >>= start Nothing . mkGlobalState opts
run' (Just dirtyFp) = do
msgOrFp <- cleanFilePath dirtyFp
case msgOrFp of
Left msg -> putStrLn msg
Right fp -> do
valOrExc <- try (readFile fp) :: IO (Either IOError String)
case valOrExc of
Left exc -> putStrLn (displayException exc)
Right val -> case parseCards val of
Left parseError -> putStr parseError
Right result ->
do gen <- createSystemRandom
makeAbsolute fp >>= addRecent
start (Just (fp, result)) (mkGlobalState opts gen)
start :: Maybe (FilePath, [Card]) -> GlobalState -> IO ()
start Nothing gs = runBrickFlashcards (gs `goToState_` mainMenuState)
start (Just (fp, cards)) gs = runBrickFlashcards =<< (gs `goToState_`) <$> cardsWithOptionsState gs fp cards
doImport :: ImportOpts -> IO ()
doImport opts' = do
let opts = opts' & optRowDelimiter %~ parseStringLiteral
valOrExc <- try $ readFile (opts ^. optInput) :: IO (Either IOError String)
case valOrExc of
Left exc -> putStrLn (displayException exc)
Right val -> do
let mCards = parseImportInput opts val
case mCards of
Right cards -> do
writeFile (opts ^. optOutput) . cardsToString $ cards
putStrLn "Successfully converted the file."
Left msg -> putStrLn msg
parseStringLiteral :: String -> String
parseStringLiteral s = case parse (many charLiteral) "" s of
Left errorBundle -> error (errorBundlePretty (errorBundle :: ParseErrorBundle String Void))
Right result -> result