packages feed

hledger-iadd 1.1 → 1.1.1

raw patch · 7 files changed

+368/−55 lines, 7 filesbinary-addedPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

+ ChangeLog.md view
@@ -0,0 +1,21 @@+# 1.1.1++ - bugfix: Show cursor in empty entry widget+ - bugfix: Correctly execute `--help` and `--dump-default-config` in+   the presence of syntax errors in the config file++# 1.1++ - Add a configuration file for persistent settings+ - Disallow unbalanced transactions+ - Order postings naturally and omit balancing amounts in transaction preview (thanks Tristan Hume)+ - Suggest account based on last transaction if no similar transaction is found (thanks Tristan Hume)+ - Make completed dates as recent as possible (thanks Thorsten Wißmann)+ - Optional fuzzy matching via config option "completion-engine" (thanks Tristan Hume)+ - Add Ctrl-d as new keybinding for 'quit'+ - Make ESC quit at the toplevel+ - Various bug fixes++# 1.0++ - Initial release
+ README.md view
@@ -0,0 +1,162 @@+# hledger-iadd++An interactive terminal UI as drop-in replacement for `hledger add`.++![Screencast](doc/screencast.gif)++## Features++This project improves in the following ways on hledger's `add` command:++ - Interactive as-you-type completion for account names and+   descriptions with optional fuzzy matching (see [below](#configuration-file)).++ - Integrated calculator: Amounts can be written as simple sums with+   real-time feedback on the result.++ - All actions while entering a transaction can be undone++ - Configurable format for date input. Instead of `y/m/d` it is also+   possible to use other formats like the german `d.m.y`.++## Installation+### stack++The easiest method would be [stack]: Install the [stack] program, `cd`+to `hledger-iadd`s source directory and type:++    stack install++To build and install all Haskell dependencies locally and install+`hledger-iadd` to `~/.local/bin`. See `stack --help` for more options.+You may get asked to install the GHC Haskell compiler locally. To do+that, type `stack setup`.++### Cabal++First, install the GHC Haskell compiler and the `cabal install`,+`alex` and `happy` build tools, possibly from your distribution or the+[haskell platform].++Since `cabal` builds regularly break in non-isolated environments, the+recommended next step is to create a cabal sandbox where all+dependencies will be installed in:++    cd hledger-iadd+	cabal sandbox init++You can now download and install all dependencies locally with++    cabal install --only-dependencies++And finally you're ready to build and install `hledger-iadd`:++    cabal configure --bindir ~/bin+	cabal build+	cabal copy++## Usage++You can start the program either with++    hledger iadd++or simply `hledger-iadd`.++The following command line options are available:++  - `-f/--file/`: Path to the journal file. (Default: `~/.hledger.journal`)+  - `--date-format`: Format for parsing dates. (Default:+    `[[%y/]%m/]%d`, the usual ledger date format). Brackets can be+    used to specify optional parts. E.g the german date format would+    be `%d[.[%m[.[%y]]]]`+  - `--completion-engine`: Algorithm for account name completion. Can+    be `substrings` or `fuzzy`.+  - `--dump-default-config`: Print the example config file to stdout+    and exit++The UI is partitioned in 4 regions:++    Current Transaction (view of your work in progress)+	---------------------------------------------------+	Question: [ text area                             ]+	---------------------------------------------------+	Context information (e.g. list of accounts)+++++	---------------------------------------------------+	Message area++For each transaction, you will get asked the following questions in+order:++ 1. Date?+ 2. Description?+ 3. Account name?+ 4. Amount?+ 5. The last two questions are repeated until you enter the empty account+ 6. Do you want to add this transaction to the journal?++To accept the default answer, immediately press <kbd>Return</kbd> at a+promt.++While you type, the context area shows possible completions. Pressing+<kbd>Return</kbd> answers the question with the currently selected+completion. You can select differnt completions with <kbd>C-n</kbd>+and <kbd>C-p</kbd>.++The following keyboard shortcuts are available:++| Key                             | Function                                                                      |+| ------------------------------- | ----------------------------------------------------------------------------- |+| <kbd>C-c</kbd>, <kbd>C-d</kbd>  | Quit the program without saving the current transaction                       |+| <kbd>Esc</kbd>                  | Abort the current transaction or exit when at toplevel                        |+| <kbd>Ret</kbd>                  | Accept the currently selected answer                                          |+| <kbd>Alt-Ret</kbd>              | Accept the current answer verbatim from the text area, ignoring the selection |+| <kbd>C-z</kbd>                  | Undo the last action                                                          |+| <kbd>Tab</kbd>                  | Insert the currently selected answer into the text area                       |+| <kbd>C-n</kbd>,<kbd>↓</kbd>     | Select the next context item                                                  |+| <kbd>C-p</kbd>,<kbd>↑</kbd>     | Select the previous context item                                              |+| <kbd>F1</kbd>,<kbd>Alt-?</kbd>  | Show help dialog                                                              |++## Default Currency++To make entry easier it is recommended that you set a [default commodity](http://hledger.org/manual.html#default-commodity)+in your ledger file if you haven't already done so.+That way when entering amounts, `hledger-iadd` will add the symbols for you.+You can do this by adding a line like below to the top of your ledger file:++```lisp+; sets the default commodity symbol and placement, thousands separator, and decimal symbol+D $1,000.00+```++## Configuration File++`hledger-iadd` is optionally configurable through a configuration file+in `${XDG_CONFIG_HOME}/hledger-iadd/config.conf`. This file consists+of simple++    key = value++assignments on individual lines with whitespace or comments starting+with `#` between them. The default config can be obtained by+passing `--dump-default-config` to `hledger-iadd`.++The following options are currently available:++  - `file`: Path to the journal file.+  - `date-format`: The date format. See the documentation for+    `--date-format` for details.+  - `completion-engine`: Algorithm used to find completions for+    account names. Possible values are:+	- `substrings`: Every word in the search string has to occur+      somewhere in the account name+	- `fuzzy`: All letters from the search string have to appear in+      the name in the same order+++[stack]: https://github.com/commercialhaskell/stack+[haskell platform]: https://www.haskell.org/platform/
+ doc/screencast.gif view

binary file changed (absent → 36538 bytes)

hledger-iadd.cabal view
@@ -1,20 +1,42 @@ name:                hledger-iadd-version:             1.1+version:             1.1.1 synopsis:            A terminal UI as drop-in replacement for hledger add-description:         Please see README.md-homepage:            http://github.com/rootzlevel/hledger-iadd#readme+description:         This is a terminal UI as drop-in replacement for hledger add.+                     .+                     It improves in the following ways on hledger's add command:+                     .+                       * Interactive as-you-type completion for+                         account names and descriptions with optional+                         fuzzy matching.+                       * Integrated calculator: Amounts can be written+                         as simple sums with real-time feedback on the+                         result.+                       * All actions while entering a transaction can+                         be undone+                       * Configurable format for date input. Instead+                         of @y\/m\/d@ it is also possible to use other+                         formats like the german @d.m.y@.++homepage:            https://github.com/hpdeifel/hledger-iadd#readme+bug-reports:         https://github.com/hpdeifel/hledger-iadd/issues license:             BSD3 license-file:        LICENSE-author:              Hans-Peter Deifel-maintainer:          hpd@hpdeifel.de+author:              Hans-Peter Deifel <hpd@hpdeifel.de>+maintainer:          Hans-Peter Deifel <hpd@hpdeifel.de> copyright:           2016 Hans-Peter Deifel category:            Finance, Console build-type:          Simple cabal-version:       >=1.10+tested-with:         GHC==7.10.3, GHC==8.0                      +extra-source-files:+  doc/screencast.gif+  README.md+  ChangeLog.md+ source-repository head   type: git-  location: https://github.com/rootzlevel/hledger-iadd.git+  location: https://github.com/hpdeifel/hledger-iadd.git    library   hs-source-dirs:      src@@ -78,6 +100,7 @@   main-is:            Spec.hs   other-modules:      DateParserSpec                     , ConfigParserSpec+                    , AmountParserSpec   default-language:   Haskell2010   build-depends:      base >= 4.8 && < 5                     , hledger-iadd
src/ConfigParser.hs view
@@ -100,7 +100,7 @@ instance Show ConfParseError where   show (SyntaxError e) = parseErrorPretty e   show (UnknownOption pos key) =-    show pos ++ ": Unknown option " ++ T.unpack key+    sourcePosPretty pos ++ ": Unknown option " ++ T.unpack key ++ "\n"   show (TypeError e) = parseErrorPretty e  -- | Class for supported option types.
src/main/Main.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings, LambdaCase #-} @@ -15,6 +17,7 @@ import           Control.Monad import           Control.Monad.IO.Class import           Control.Monad.Trans.Except+import           Data.Functor.Identity import           Data.Maybe import           Data.Monoid import           Data.Text (Text)@@ -25,8 +28,8 @@ import qualified Hledger as HL import qualified Hledger.Read.JournalReader as HL import           Lens.Micro-import           Options.Applicative hiding (str, option) import qualified Options.Applicative as OA+import           Options.Applicative hiding (str, option) import           System.Directory import           System.Environment.XDG.BaseDir import           System.Exit@@ -261,7 +264,53 @@ addToJournal :: HL.Transaction -> FilePath -> IO () addToJournal trans path = appendFile path (HL.showTransaction trans) +--------------------------------------------------------------------------------+-- Command line and config parsing+-------------------------------------------------------------------------------- +data CommonOptions f = CommonOptions+  { optLedgerFile :: f FilePath+  , optDateFormat :: f String+  , optMatchAlgo  :: f MatchAlgo+  }++instance Monoid (CommonOptions Maybe) where+  mappend opt1 opt2 =+    let opt1' = optNatTrans First opt1+        opt2' = optNatTrans First opt2+    in optNatTrans getFirst $ CommonOptions+       { optLedgerFile = optLedgerFile opt1' <> optLedgerFile opt2'+       , optDateFormat = optDateFormat opt1' <> optDateFormat opt2'+       , optMatchAlgo = optMatchAlgo opt1' <> optMatchAlgo opt2'+       }++  mempty = CommonOptions Nothing Nothing Nothing++optNatTrans :: Functor f => (forall a. f a -> g a) -> CommonOptions f -> CommonOptions g+optNatTrans nat opts = CommonOptions+  { optLedgerFile = nat $ optLedgerFile opts+  , optDateFormat = nat $ optDateFormat opts+  , optMatchAlgo = nat $ optMatchAlgo opts+  }++optFromJust :: CommonOptions Identity -> CommonOptions Maybe -> CommonOptions Identity+optFromJust def opts =+  optNatTrans (Identity . fromJust) ( opts <> optNatTrans (Just . runIdentity) def)++data CmdLineOptions = CmdLineOptions+  { cmdCommon :: CommonOptions Maybe+  , cmdDumpConfig :: Bool+  }++data ConfOptions = ConfOptions { confCommon :: CommonOptions Maybe }++defaultOptions :: FilePath -> CommonOptions Identity+defaultOptions home = CommonOptions+  { optLedgerFile = Identity (ledgerPath home)+  , optDateFormat = Identity "[[%y/]%m/]%d"+  , optMatchAlgo = Identity Substrings+  }+ ledgerPath :: FilePath -> FilePath ledgerPath home = home <> "/.hledger.journal" @@ -282,98 +331,107 @@       | str == "substrings" = return Substrings       | otherwise = Left "Expected \"fuzzy\" or \"substrings\"" -data Options = Options-  { optLedgerFile :: FilePath-  , optDateFormat :: String-  , optMatchAlgo :: MatchAlgo-  , optDumpConfig :: Bool-  }--confParser :: FilePath -> OptParser Options-confParser home = Options+-- | Parser for our config file+confParser :: CommonOptions Identity -> OptParser ConfOptions+confParser def = fmap ConfOptions $ CommonOptions   -- TODO Convert leading tilde to home-  <$> option "file" (ledgerPath home) "Path to the journal file"-  <*> option "date-format" "[[%y/]%m/]%d" "Format used to parse dates"-  <*> customOption "completion-engine" Substrings "substrings"+  <$> (Just <$> option "file" (runIdentity $ optLedgerFile def) "Path to the journal file")+  <*> (Just <$> option "date-format" (runIdentity $ optDateFormat def) "Format used to parse dates")+  <*> (Just <$> customOption "completion-engine" matchAlgo (T.toLower $ T.pack $ show matchAlgo)       ( "Algorithm used to find completions for account names. Possible values are:\n"       <> "  - substrings: Every word in the search string has to occur somewhere in the account name\n"       <> "  - fuzzy: All letters from the search string have to appear in the name in the same order"       )       "string"       parseMatchAlgo-  <*> pure False+      ) -parseConfigFile :: IO Options+  where matchAlgo = runIdentity (optMatchAlgo def)++-- | IO Action to read and parse config file+parseConfigFile :: IO ConfOptions parseConfigFile = do   path <- configPath   home <- getHomeDirectory+  let def = defaultOptions home    try (T.readFile path) >>= \case-    Left (_ :: SomeException) -> return (parserDefault $ confParser home)-    Right res -> case parseConfig path res (confParser home) of+    Left (_ :: SomeException) -> return (parserDefault $ confParser def)+    Right res -> case parseConfig path res (confParser def) of       Left err -> do         putStr (show err)         exitFailure       Right res' -> return res' -optionParser :: Options -> Parser Options-optionParser def = Options-  <$> strOption-        (  long "file"-        <> short 'f'-        <> metavar "FILE"-        <> value (optLedgerFile def)-        <> help "Path to the journal file"-        )-  <*> strOption-        (  long "date-format"-        <> metavar "FORMAT"-        <> value (optDateFormat def)-        <> help "Format used to parse dates"-        )-  <*> OA.option readMatchAlgo-        (  long "completion-engine"-        <> metavar "ENGINE"-        <> value (optMatchAlgo def)-        <> help "Algorithm for account name completion. Possible values: \"fuzzy\", \"substrings\"")+-- | command line option parser+cmdOptionParser :: Parser CmdLineOptions+cmdOptionParser = CmdLineOptions+  <$> (CommonOptions+       <$> OA.option (Just <$> OA.str)+           (  long "file"+           <> short 'f'+           <> metavar "FILE"+           <> value Nothing+           <> help "Path to the journal file"+           )+       <*> OA.option (Just <$> OA.str)+             (  long "date-format"+             <> metavar "FORMAT"+             <> value Nothing+             <> help "Format used to parse dates"+             )+      <*> OA.option (Just <$> readMatchAlgo)+            (  long "completion-engine"+            <> metavar "ENGINE"+            <> value Nothing+            <> help "Algorithm for account name completion. Possible values: \"fuzzy\", \"substrings\"")+      )   <*> switch         ( long "dump-default-config"        <> help "Print an example configuration file to stdout and exit"         ) +--------------------------------------------------------------------------------+-- main+--------------------------------------------------------------------------------+ main :: IO () main = do-  opts1 <- parseConfigFile+  home <- getHomeDirectory+  path <- configPath+  let defOpts = defaultOptions home -  opts <- execParser $ info (helper <*> optionParser opts1) $-            fullDesc <> header "A terminal UI as drop-in replacement for hledger add."+  cmdOpts <- execParser $ info (helper <*> cmdOptionParser) $+               fullDesc <> header "A terminal UI as drop-in replacement for hledger add." -  when (optDumpConfig opts) $ do-    home <- getHomeDirectory-    path <- configPath+  when (cmdDumpConfig cmdOpts) $ do     T.putStrLn $ "# Write this to " <> T.pack path <> "\n"-    T.putStrLn (parserExample $ confParser home)+    T.putStrLn (parserExample $ confParser defOpts)     exitSuccess -  date <- case parseDateFormat (T.pack $ optDateFormat opts) of+  confOpts <- parseConfigFile++  let opts = optFromJust defOpts $ cmdCommon cmdOpts <> confCommon confOpts++  date <- case parseDateFormat (T.pack $ runIdentity $ optDateFormat opts) of     Left err -> do       hPutStr stderr "Could not parse date format: "       T.hPutStr stderr err       exitWith (ExitFailure 1)     Right res -> return res -  let path = optLedgerFile opts+  let path = runIdentity $ optLedgerFile opts   journalContents <- T.readFile path    runExceptT (HL.parseAndFinaliseJournal HL.journalp True path journalContents) >>= \case     Left err -> hPutStrLn stderr err >> exitFailure     Right journal -> do-      let edit = editor EditorName (txt . T.concat) (Just 1) ""+      let edit = editor EditorName (nonEmptyTxt . T.concat) (Just 1) ""        sugg <- suggest journal date DateQuestion        let welcome = "Welcome! Press F1 (or Alt-?) for help. Exit with Ctrl-d."-          matchAlgo = optMatchAlgo opts+          matchAlgo = runIdentity $ optMatchAlgo opts           as = AppState edit DateQuestion journal (ctxList V.empty) sugg welcome path date matchAlgo NoDialog        void $ defaultMain app as@@ -390,3 +448,12 @@  ctxList :: V.Vector e -> List Name e ctxList v = (if V.null v then id else listMoveTo 0) $ list ListName v 1++-- | Workaround for bug in brick's edit widget+--+-- Since brick version 0.15, an empty edit widget wouldn't show a cursor, so+-- this artificially adds a space to empty input text.+nonEmptyTxt :: Text -> Widget n+nonEmptyTxt input+  | T.null input = txt " "+  | otherwise    = txt input
+ tests/AmountParserSpec.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}++module AmountParserSpec (spec) where++import           Test.Hspec++import           Control.Monad.Trans.State.Strict+import           Data.Either+import           Data.Functor.Identity+import           Data.Text (Text)+import qualified Hledger as HL+import           Text.Megaparsec++import           AmountParser++spec :: Spec+spec = describe "parseAmount" $ do+  it "parses single amount" $+    parseAmount HL.nulljournal "42" `shouldBe` Right (amount "42")++  it "parses a positive number" $+    parseAmount HL.nulljournal "+42" `shouldBe` Right (amount "42")++  it "parses a negative number" $+    parseAmount HL.nulljournal "-42" `shouldBe` Right (amount "-42")++  it "parses a simple sum" $+    parseAmount HL.nulljournal "23 + 23 + 42 + 1" `shouldBe` Right (amount "89")++  it "parses a sum with negative values" $+    parseAmount HL.nulljournal "-42 + 23 + 42 - 23 + 1" `shouldBe` Right (amount "1")++  it "fails to parse a trailing plus" $+    parseAmount HL.nulljournal "23 +" `shouldSatisfy` isLeft++amount :: Text -> HL.MixedAmount+amount = HL.mixed . pure . fromRight . runIdentity . runParserT (evalStateT HL.amountp HL.nulljournal) ""++fromRight :: Either a b -> b+fromRight = either (error "fromRight: Left value encountered") id