hascard 0.1.2.0 → 0.1.4.0
raw patch · 10 files changed
+153/−63 lines, 10 filesdep +mwc-randomdep +random-fuPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: mwc-random, random-fu
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: GlobalState :: GenIO -> Bool -> Maybe Int -> GlobalState
+ UI: [_doShuffle] :: GlobalState -> Bool
+ UI: [_mwc] :: GlobalState -> GenIO
+ UI: [_subset] :: GlobalState -> Maybe Int
+ UI: data GlobalState
+ UI: doShuffle :: Lens' GlobalState Bool
+ UI: mwc :: Lens' GlobalState GenIO
+ UI: runCardsWithOptions :: GlobalState -> [Card] -> IO ()
+ UI: subset :: Lens' GlobalState (Maybe Int)
+ UI.CardSelector: runCardsWithOptions :: GlobalState -> [Card] -> IO ()
+ UI.Cards: data Card
- UI: runBrickFlashcards :: IO ()
+ UI: runBrickFlashcards :: GlobalState -> IO ()
- UI: runCardSelectorUI :: IO ()
+ UI: runCardSelectorUI :: GlobalState -> IO ()
- UI: runCardsUI :: [Card] -> IO State
+ UI: runCardsUI :: GlobalState -> [Card] -> IO State
- UI: runMainMenuUI :: IO ()
+ UI: runMainMenuUI :: GlobalState -> IO ()
- UI.CardSelector: runCardSelectorUI :: IO ()
+ UI.CardSelector: runCardSelectorUI :: GlobalState -> IO ()
- UI.Cards: runCardsUI :: [Card] -> IO State
+ UI.Cards: runCardsUI :: GlobalState -> [Card] -> IO State
- UI.MainMenu: runMainMenuUI :: IO ()
+ UI.MainMenu: runMainMenuUI :: GlobalState -> IO ()
Files
- ChangeLog.md +14/−2
- README.md +15/−4
- app/Main.hs +42/−23
- hascard.cabal +9/−3
- src/Parser.hs +5/−3
- src/Types.hs +10/−0
- src/UI.hs +3/−3
- src/UI/CardSelector.hs +31/−8
- src/UI/Cards.hs +3/−3
- src/UI/MainMenu.hs +21/−14
ChangeLog.md view
@@ -1,13 +1,25 @@ # Changelog for hascard+## 0.1.4.0+New:+- Deleted or moved files are also no longer shown in the recently selected files list +Fixed bugs:+- A new line after the last seperator `---` caused the parsing to fail, now it does not+- If the last card was an open question and there was no `---` at the end, it was shown as a definition card (issue #6, thanks @stemvork)++## 0.1.3.0+New:+- Add `-s` shuffle flag and `-a` amount option to CLI.+- Add macOS binary and homebrew formula+ ## 0.1.2.0 New:-- Directories are now hidden by default, and can be shown by pressing 'h' in the filebrowser+- Hidden files/directories (so starting with '.') are now hidden by default, and can be shown by pressing 'h' in the filebrowser Fixed bugs: - Parsing now succeeds even if the text file does not end with ----- When passing a file via the CLI, the absolute path is saved instead of the relative one, preventing issues with the 'recently selected files'+- When passing a file to the CLI, the absolute path is saved instead of the relative one, preventing issues with the 'recently selected files' ## 0.1.1.0 New:
README.md view
@@ -14,9 +14,17 @@ - [Miscellaneous info](#miscellaneous-info) ## Installation-Installation on Windows is not possible sadly, aside from WSL. This is because hascard depends on vty which only supports unix operating systems (this includes MacOS). +Installation on Windows is not possible sadly, aside from WSL. This is because hascard depends on vty which only supports unix operating systems (this includes macOS).++### Homebrew (for macOS)+For macOS users an installation using homebrew is provided via a custom tap. You can run +```+brew update+brew install Yvee1/tools/hascard+```+ ### Binary-The binary used on my system is available under [releases](https://github.com/Yvee1/hascard/releases/). If you run debian with the x86-64 architecture that binary should work for you too. To be able to run it from any directory, it has to be added to the PATH. This can be done by copying it to e.g. the `/usr/local/bin` directory.+Linux and macOS binaries are available under [releases](https://github.com/Yvee1/hascard/releases/). To be able to run it from any directory, it has to be added to the PATH. This can be done by copying it to e.g. the `/usr/local/bin` directory. ### Snapcraft Hascard is also on [snapcraft](https://snapcraft.io/hascard). Installation instructions are on that site. If you already have snap installed you can just install hascard via `sudo snap install hascard`. By default snap applications are isolated from the system and run in a sandbox. This means that hascard does not have permission to read or write any files on the system aside from those under `%HOME/snap/hascard`. To be able to read cards also in other directories under the home directory, hascard makes use of the `home` interface which might need to be enabled manually using `sudo snap connect hascard:home :home`.@@ -32,8 +40,11 @@ and do `stack install hascard` or `nix-build` respectively. ## Usage-Simply run `hascard` to open the main application. Menu navigation can be done with the arrow keys or with the 'j' and 'k' keys. The controls for the different cards can be found at the bottom of the screen by default. This, and a couple other things, can be changed in the settings menu. Currently there is no functionality for making cards inside the hascard application itself, but they can easily be written in your favorite text editor since the syntax is human-friendly. Instead of selecting a deck of flashcards via the built-in filebrowser, it's also possible to run `hascard [FILE.txt]` where FILE is the text file with cards.+Simply run `hascard` to open the main application. Menu navigation can be done with the arrow keys or with the 'j' and 'k' keys. The controls for the different cards can be found at the bottom of the screen by default. This, and a couple other things, can be changed in the settings menu. Currently there is no functionality for making cards inside the hascard application itself, but they can easily be written in your favorite text editor since the syntax is human-friendly. +### CLI+The CLI provides some options for running hascard; to see them all run `hascard -h`. As an example, say you have a file `deck.txt` with lots of cards in it and you want to review 5 random ones, you can use `hascard deck -s -a 5`. Here `-s` shuffles the deck and `-a 5` specifies we only want to look at 5 of them.+ ## Cards Decks of cards are written in `.txt` files. Cards are seperated with a line containing three dashes `---`. For examples, see the [`/cards`](https://github.com/Yvee1/hascard/tree/master/cards) directory. In this section the 4 different cards are listed, with the syntax and how it is represented in the application. @@ -84,4 +95,4 @@  ## Miscellaneous info-Written in Haskell, UI built with [brick](https://github.com/jtdaugherty/brick) and parsing of cards done with [parsec](https://github.com/haskell/parsec). Recordings of the terminal were made using [terminalizer](https://github.com/faressoft/terminalizer). The filebrowser widget was mostly copied from the brick [filebrowser demo program](https://github.com/jtdaugherty/brick/blob/master/programs/FileBrowserDemo.hs).+Written in Haskell, UI built with [brick](https://github.com/jtdaugherty/brick) and parsing of cards done with [parsec](https://github.com/haskell/parsec). Recordings of the terminal were made using [terminalizer](https://github.com/faressoft/terminalizer). The filebrowser widget was mostly copied from the brick [filebrowser demo program](https://github.com/jtdaugherty/brick/blob/master/programs/FileBrowserDemo.hs). Homebrew and Travis configurations were made much easier by [the tutorial from Chris Penner](https://chrispenner.ca/posts/homebrew-haskell).
app/Main.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TemplateHaskell #-} module Main where import UI@@ -5,31 +6,39 @@ import Control.Monad (void, when) import Data.Functor (($>)) import Data.Version (showVersion)+import Lens.Micro.Platform import Paths_hascard (version) import Parser import Options.Applicative import System.Directory (makeAbsolute)-import System.Process (runCommand) import System.FilePath (takeExtension)+import System.Process (runCommand)+import System.Random.MWC (createSystemRandom, GenIO) data Opts = Opts- { optFile :: Maybe String- , optVersion :: Bool+ { _optFile :: Maybe String+ , _optSubset :: Int+ , _optShuffle :: Bool+ , _optVersion :: Bool } +makeLenses ''Opts+ main :: IO () main = do useEscapeCode <- getUseEscapeCode when useEscapeCode $ void (runCommand "echo -n \\\\e[5 q") options <- execParser optsWithHelp- if optVersion options+ if options ^. optVersion then putStrLn (showVersion version)- else run $ optFile options+ else run options opts :: Parser Opts opts = Opts- <$> optional (argument str (metavar "FILE.txt" <> help "A .txt file containing flashcards"))+ <$> 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))+ <*> switch (long "shuffle" <> short 's' <> help "Randomize card order") <*> switch (long "version" <> short 'v' <> help "Show version number") optsWithHelp :: ParserInfo Opts@@ -37,20 +46,30 @@ fullDesc <> progDesc "Run the normal application without argument, or run it directly on a deck of flashcards by providing a text file." <> header "Hascard - a TUI for reviewing notes" -run :: Maybe String -> IO ()-run Nothing = runBrickFlashcards-run (Just file) = do- 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" - 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 -> (makeAbsolute textfile >>= addRecent) *> runCardsUI result $> ()+nothingIf :: (a -> Bool) -> a -> Maybe a+nothingIf p a+ | p a = Nothing+ | otherwise = Just a++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+ run' (Just file) = do+ 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" + 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 -> + do gen <- createSystemRandom+ (makeAbsolute textfile >>= addRecent) *> runCardsWithOptions (mkGlobalState gen) result $> ()
hascard.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2.+-- This file has been generated from package.yaml by hpack version 0.33.0. -- -- see: https://github.com/sol/hpack ----- hash: b0ef7eaa1b6c53ffbbff8d10a8ea1a4e49ab9dc738a36e190346b422ff795ba3+-- hash: a9024c106965452173618e8284b174a2d9eed387ee8d472593b02ef6472e1bb4 name: hascard-version: 0.1.2.0+version: 0.1.4.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@@ -53,10 +53,12 @@ , filepath >=1.4.2 && <1.5 , 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 , text >=1.2.3 && <1.3 , vector >=0.12.0 && <0.13@@ -80,10 +82,12 @@ , hascard , 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 , text >=1.2.3 && <1.3 , vector >=0.12.0 && <0.13@@ -108,10 +112,12 @@ , hascard , 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 , text >=1.2.3 && <1.3 , vector >=0.12.0 && <0.13
src/Parser.hs view
@@ -1,4 +1,3 @@--- {-# LANGUAGE RankNTypes #-} {-# LANGUAGE DataKinds, ExistentialQuantification, GADTs, KindSignatures #-} module Parser (parseCards) where @@ -82,7 +81,7 @@ <|> string "_" pNormal = do- text <- manyTill (noneOf "_") $ lookAhead (try gappedSpecialChars)+ text <- manyTill (noneOf "_") $ lookAhead $ try $ gappedSpecialChars <|> (eof >> return []) return (Normal text) pDef = do@@ -97,7 +96,10 @@ <|> string "\r" <?> "end of line" -seperator = string "---"+seperator = do+ sep <- string "---"+ many eol+ return sep notEOL = noneOf "\n\r"
src/Types.hs view
@@ -1,5 +1,15 @@+{-# 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
src/UI.hs view
@@ -1,10 +1,10 @@-module UI (module X, runBrickFlashcards) where+module UI (module X, module UI) 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) -runBrickFlashcards :: IO ()+runBrickFlashcards :: GlobalState -> IO () runBrickFlashcards = runMainMenuUI-
src/UI/CardSelector.hs view
@@ -1,21 +1,30 @@ {-# LANGUAGE TemplateHaskell #-}-module UI.CardSelector (runCardSelectorUI, getRecents, getRecentsFile, addRecent) where+module UI.CardSelector + (runCardSelectorUI+ , getRecents+ , getRecentsFile+ , addRecent+ , runCardsWithOptions) 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 UI.BrickHelpers import UI.FileBrowser (runFileBrowserUI)-import UI.Cards (runCardsUI)+import UI.Cards (runCardsUI, Card) import qualified Brick.Widgets.List as L import qualified Data.Vector as Vec import qualified Graphics.Vty as V@@ -29,6 +38,7 @@ { _list :: L.List Name String , _exception :: Maybe String , _recents :: Stack FilePath+ , _gs :: GlobalState } makeLenses ''State@@ -117,18 +127,18 @@ Left parseError -> continue (s' & exception ?~ show parseError) Right result -> suspendAndResume $ do s'' <- addRecentInternal s' fp- _ <- runCardsUI result+ _ <- runCardsWithOptions (s^.gs) result return (s'' & exception .~ Nothing) _ -> continue s' handleEvent l _ = continue l -runCardSelectorUI :: IO ()-runCardSelectorUI = do+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+ let initialState = State (L.list () options 1) Nothing rs gs _ <- defaultMain app initialState return () @@ -137,9 +147,16 @@ rf <- getRecentsFile exists <- D.doesFileExist rf if exists- then S.fromList . lines <$> IOS.readFile rf+ then removeDeletedFiles rf else return S.empty +removeDeletedFiles :: FilePath -> IO (Stack FilePath)+removeDeletedFiles fp = do+ file <- IOS.readFile fp+ existing <- S.fromList <$> filterM D.doesFileExist (lines file)+ writeRecents existing+ return existing+ maxRecents :: Int maxRecents = 5 @@ -224,4 +241,10 @@ runFileBrowser :: State -> IO State runFileBrowser s = do result <- runFileBrowserUI- maybe (return s) (\(cards, fp) -> addRecentInternal s fp <* runCardsUI cards) result+ 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,5 +1,5 @@ {-# LANGUAGE TemplateHaskell #-}-module UI.Cards (runCardsUI) where+module UI.Cards (runCardsUI, Card) where import Brick import Lens.Micro.Platform@@ -418,8 +418,8 @@ , (gapAttr, V.defAttr `V.withStyle` V.underline) ] -runCardsUI :: [Card] -> IO State-runCardsUI deck = do+runCardsUI :: GlobalState -> [Card] -> IO State+runCardsUI gs deck = do hints <- getShowHints controls <- getShowControls
src/UI/MainMenu.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TemplateHaskell #-} module UI.MainMenu (runMainMenuUI) where import Brick@@ -5,6 +6,8 @@ import Brick.Widgets.Border.Style import Brick.Widgets.Center import Data.Functor (($>))+import Lens.Micro.Platform+import Types (GlobalState) import UI.BrickHelpers import UI.CardSelector import UI.Info@@ -15,8 +18,12 @@ type Event = () type Name = ()-type State = L.List Name String+data State = State + { _l :: L.List Name String+ , _gs :: GlobalState } +makeLenses ''State+ app :: App State Event Name app = App { appDraw = drawUI@@ -49,7 +56,7 @@ drawList :: State -> Widget Name drawList s = vLimit 4 $- L.renderList drawListElement True s+ L.renderList drawListElement True (s^.l) drawListElement :: Bool -> String -> Widget Name drawListElement selected = hCenteredStrWrapWithAttr attr@@ -68,29 +75,29 @@ , (titleAttr, fg V.yellow) ] handleEvent :: State -> BrickEvent Name Event -> EventM Name (Next State)-handleEvent l (VtyEvent e) =+handleEvent s (VtyEvent e) = case e of- V.EvKey (V.KChar 'c') [V.MCtrl] -> halt l- V.EvKey V.KEsc [] -> halt l+ V.EvKey (V.KChar 'c') [V.MCtrl] -> halt s+ V.EvKey V.KEsc [] -> halt s V.EvKey V.KEnter [] ->- case L.listSelected l of- Just 0 -> suspendAndResume $ runCardSelectorUI $> l- Just 1 -> suspendAndResume $ runInfoUI $> l- Just 2 -> suspendAndResume $ runSettingsUI $> l- Just 3 -> halt l+ 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 _ -> undefined - ev -> continue =<< L.handleListEventVi L.handleListEvent ev l+ ev -> continue . flip (l .~) s =<< L.handleListEventVi L.handleListEvent ev (s^.l) handleEvent l _ = continue l -runMainMenuUI :: IO ()-runMainMenuUI = do+runMainMenuUI :: GlobalState -> IO ()+runMainMenuUI gs = do let options = Vec.fromList [ "Select" , "Info" , "Settings" , "Quit" ] - let initialState = L.list () options 1+ let initialState = State (L.list () options 1) gs _ <- defaultMain app initialState return ()