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 +21/−0
- README.md +162/−0
- doc/screencast.gif binary
- hledger-iadd.cabal +29/−6
- src/ConfigParser.hs +1/−1
- src/main/Main.hs +115/−48
- tests/AmountParserSpec.hs +40/−0
+ 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`.++++## 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