hledger 0.10 → 0.11
raw patch · 25 files changed
+1593/−803 lines, 25 filesdep +convertible-textdep +yesoddep −testpackdep ~hledger-libdep ~io-storagePVP ok
version bump matches the API change (PVP)
Dependencies added: convertible-text, yesod
Dependencies removed: testpack
Dependency ranges changed: hledger-lib, io-storage
API changes (from Hackage documentation)
- Hledger.Cli.Commands.Stats: showStats :: [Opt] -> [String] -> Ledger -> Day -> String
- Hledger.Cli.Utils: journalFromStringWithOpts :: [Opt] -> String -> IO Journal
+ Hledger.Cli.Commands.Balance: exclusiveBalance :: Account -> MixedAmount
+ Hledger.Cli.Commands.Balance: isInterestingFlat :: [Opt] -> Ledger -> AccountName -> Bool
+ Hledger.Cli.Commands.Balance: isInterestingIndented :: [Opt] -> Ledger -> AccountName -> Bool
+ Hledger.Cli.Commands.Stats: showLedgerStats :: [Opt] -> [String] -> Ledger -> Day -> DateSpan -> String
+ Hledger.Cli.Options: BaseUrl :: String -> Opt
+ Hledger.Cli.Options: Drop :: String -> Opt
+ Hledger.Cli.Options: Flat :: Opt
+ Hledger.Cli.Options: HelpAll :: Opt
+ Hledger.Cli.Options: HelpOptions :: Opt
+ Hledger.Cli.Options: Port :: String -> Opt
+ Hledger.Cli.Options: baseUrlFromOpts :: [Opt] -> Maybe String
+ Hledger.Cli.Options: dropFromOpts :: [Opt] -> Int
+ Hledger.Cli.Options: portFromOpts :: [Opt] -> Maybe Int
+ Hledger.Cli.Utils: journalFileIsNewer :: Journal -> IO Bool
+ Hledger.Cli.Utils: journalFileModificationTime :: Journal -> IO ClockTime
+ Hledger.Cli.Utils: journalReload :: Journal -> IO (Either String Journal)
+ Hledger.Cli.Utils: journalReloadIfChanged :: [Opt] -> Journal -> IO (Either String Journal, Bool)
+ Hledger.Cli.Utils: readJournalWithOpts :: [Opt] -> String -> IO Journal
+ Hledger.Cli.Utils: writeFileWithBackup :: FilePath -> String -> IO ()
+ Hledger.Cli.Utils: writeFileWithBackupIfChanged :: FilePath -> String -> IO Bool
- Hledger.Cli.Commands.Balance: showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String
+ Hledger.Cli.Commands.Balance: showInterestingAccount :: [Opt] -> Ledger -> [AccountName] -> AccountName -> String
Files
- CONTRIBUTORS.rst +1/−0
- Hledger/Cli/Commands/Add.hs +11/−9
- Hledger/Cli/Commands/All.hs +18/−12
- Hledger/Cli/Commands/Balance.hs +29/−11
- Hledger/Cli/Commands/Convert.hs +5/−4
- Hledger/Cli/Commands/Print.hs +1/−1
- Hledger/Cli/Commands/Register.hs +24/−7
- Hledger/Cli/Commands/Stats.hs +26/−17
- Hledger/Cli/Commands/Vty.hs +2/−2
- Hledger/Cli/Commands/Web.hs +396/−299
- Hledger/Cli/Commands/Web610.hs +316/−0
- Hledger/Cli/Main.hs +18/−4
- Hledger/Cli/Options.hs +83/−39
- Hledger/Cli/Tests.hs +61/−51
- Hledger/Cli/Utils.hs +93/−29
- Hledger/Cli/Version.hs +3/−3
- MANUAL.markdown +367/−202
- NEWS.rst +31/−0
- README.rst +4/−4
- README2.rst +10/−8
- data/sample.journal +40/−0
- data/sample.ledger +0/−40
- data/sample.rules +1/−1
- data/web/style.css +19/−7
- hledger.cabal +34/−53
CONTRIBUTORS.rst view
@@ -12,6 +12,7 @@ - Sergey Astanin - utf8 support - Nick Ingolia - parser improvements - Roman Cheplyaka - "chart" command, "add" command improvements+- Michael Snoyman - some additions to the Yesod web interface Developers who have not yet signed the contributor agreement:
Hledger/Cli/Commands/Add.hs view
@@ -8,6 +8,7 @@ module Hledger.Cli.Commands.Add where import Hledger.Data+import Hledger.Read.Journal (someamount) import Hledger.Cli.Options import Hledger.Cli.Commands.Register (showRegisterReport) #if __GLASGOW_HASKELL__ <= 610@@ -19,21 +20,22 @@ #endif import System.IO.Error import Text.ParserCombinators.Parsec-import Hledger.Cli.Utils (journalFromStringWithOpts)+import Hledger.Cli.Utils (readJournalWithOpts) import qualified Data.Foldable as Foldable (find) --- | Read ledger transactions from the terminal, prompting for each field,--- and append them to the ledger file. If the ledger came from stdin, this+-- | Read transactions from the terminal, prompting for each field,+-- and append them to the journal file. If the journal came from stdin, this -- command has no effect. add :: [Opt] -> [String] -> Journal -> IO () add opts args j- | filepath j == "-" = return ()+ | f == "-" = return () | otherwise = do hPutStrLn stderr $- "Enter one or more transactions, which will be added to your ledger file.\n"+ "Enter one or more transactions, which will be added to your journal file.\n" ++"To complete a transaction, enter . as account name. To quit, press control-c." today <- getCurrentDay getAndAddTransactions j opts args today `catch` (\e -> unless (isEOFError e) $ ioError e)+ where f = filepath j -- | Read a number of transactions from the command line, prompting, -- validating, displaying and appending them to the journal file, until@@ -53,7 +55,7 @@ (Just $ showDate defaultDate) (Just $ \s -> null s || isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))- description <- askFor "description" Nothing (Just $ not . null) + description <- askFor "description" (Just "") Nothing let historymatches = transactionsSimilarTo j args description bestmatch | null historymatches = Nothing | otherwise = Just $ snd $ head historymatches@@ -147,17 +149,17 @@ then putStr $ sep ++ s else appendFile f $ sep++s where - -- XXX we are looking at the original raw text from when the ledger+ -- XXX we are looking at the original raw text from when the journal -- was first read, but that's good enough for now sep | null $ strip t = "" | otherwise = replicate (2 - min 2 (length lastnls)) '\n' where lastnls = takeWhile (=='\n') $ reverse t --- | Convert a string of ledger data into a register report.+-- | Convert a string of journal data into a register report. registerFromString :: String -> IO String registerFromString s = do now <- getCurrentLocalTime- l <- journalFromStringWithOpts [] s+ l <- readJournalWithOpts [] s return $ showRegisterReport opts (optsToFilterSpec opts [] now) l where opts = [Empty]
Hledger/Cli/Commands/All.hs view
@@ -15,14 +15,16 @@ module Hledger.Cli.Commands.Print, module Hledger.Cli.Commands.Register, module Hledger.Cli.Commands.Stats,+#ifdef CHART+ module Hledger.Cli.Commands.Chart,+#endif #ifdef VTY module Hledger.Cli.Commands.Vty, #endif-#if defined(WEB) || defined(WEBHAPPSTACK)+#if defined(WEB) module Hledger.Cli.Commands.Web,-#endif-#ifdef CHART- module Hledger.Cli.Commands.Chart,+#elif defined(WEB610)+ module Hledger.Cli.Commands.Web610, #endif tests_Hledger_Commands )@@ -34,14 +36,16 @@ import Hledger.Cli.Commands.Print import Hledger.Cli.Commands.Register import Hledger.Cli.Commands.Stats+#ifdef CHART+import Hledger.Cli.Commands.Chart+#endif #ifdef VTY import Hledger.Cli.Commands.Vty #endif-#if defined(WEB) || defined(WEBHAPPSTACK)+#if defined(WEB) import Hledger.Cli.Commands.Web-#endif-#ifdef CHART-import Hledger.Cli.Commands.Chart+#elif defined(WEB610)+import Hledger.Cli.Commands.Web610 #endif import Test.HUnit (Test(TestList)) @@ -56,12 +60,14 @@ ,Hledger.Cli.Commands.Register.tests_Register -- ,Hledger.Cli.Commands.Stats.tests_Stats ]+-- #ifdef CHART+-- ,Hledger.Cli.Commands.Chart.tests_Chart+-- #endif -- #ifdef VTY -- ,Hledger.Cli.Commands.Vty.tests_Vty -- #endif--- #if defined(WEB) || defined(WEBHAPPSTACK)+-- #if defined(WEB) -- ,Hledger.Cli.Commands.Web.tests_Web--- #endif--- #ifdef CHART--- ,Hledger.Cli.Commands.Chart.tests_Chart+-- #elif defined(WEB610)+-- ,Hledger.Cli.Commands.Web610.tests_Web -- #endif
Hledger/Cli/Commands/Balance.hs view
@@ -4,7 +4,7 @@ A ledger-compatible @balance@ command. ledger's balance command is easy to use but not easy to describe-precisely. In the examples below we'll use sample.ledger, which has the+precisely. In the examples below we'll use sample.journal, which has the following account tree: @@@ -29,7 +29,7 @@ subaccounts: @- $ hledger -f sample.ledger balance+ $ hledger -f sample.journal balance $-1 assets $1 bank:saving $-2 cash@@ -52,7 +52,7 @@ So, to see just the top level accounts: @-$ hledger -f sample.ledger balance --depth 1+$ hledger -f sample.journal balance --depth 1 $-1 assets $2 expenses $-2 income@@ -67,7 +67,7 @@ (elided) and subaccounts. So with the pattern o we get: @- $ hledger -f sample.ledger balance o+ $ hledger -f sample.journal balance o $1 expenses:food $-2 income $-1 gifts@@ -116,28 +116,32 @@ t <- getCurrentLocalTime putStr $ showBalanceReport opts (optsToFilterSpec opts args t) j --- | Generate a balance report with the specified options for this ledger.+-- | Generate a balance report with the specified options for this journal. showBalanceReport :: [Opt] -> FilterSpec -> Journal -> String showBalanceReport opts filterspec j = acctsstr ++ totalstr where l = journalToLedger filterspec j acctsstr = unlines $ map showacct interestingaccts where- showacct = showInterestingAccount l interestingaccts+ showacct = showInterestingAccount opts l interestingaccts interestingaccts = filter (isInteresting opts l) acctnames acctnames = sort $ tail $ flatten $ treemap aname accttree accttree = ledgerAccountTree (fromMaybe 99999 $ depthFromOpts opts) l totalstr | NoTotal `elem` opts = ""- | notElem Empty opts && isZeroMixedAmount total = "" | otherwise = printf "--------------------\n%s\n" $ padleft 20 $ showMixedAmountWithoutPrice total where total = sum $ map abalance $ ledgerTopAccounts l -- | Display one line of the balance report with appropriate indenting and eliding.-showInterestingAccount :: Ledger -> [AccountName] -> AccountName -> String-showInterestingAccount l interestingaccts a = concatTopPadded [amt, " ", depthspacer ++ partialname]+showInterestingAccount :: [Opt] -> Ledger -> [AccountName] -> AccountName -> String+showInterestingAccount opts l interestingaccts a = concatTopPadded [amt, " ", name] where- amt = padleft 20 $ showMixedAmountWithoutPrice $ abalance $ ledgerAccount l a+ amt = padleft 20 $ showMixedAmountWithoutPrice bal+ bal | Flat `elem` opts = exclusiveBalance acct+ | otherwise = abalance acct+ acct = ledgerAccount l a+ name | Flat `elem` opts = accountNameDrop (dropFromOpts opts) a+ | otherwise = depthspacer ++ partialname parents = parentAccountNames a interestingparents = filter (`elem` interestingaccts) parents depthspacer = replicate (indentperlevel * length interestingparents) ' '@@ -147,9 +151,23 @@ partialname = accountNameFromComponents $ reverse (map accountLeafName ps) ++ [accountLeafName a] where ps = takeWhile boring parents where boring = not . (`elem` interestingparents) +exclusiveBalance :: Account -> MixedAmount+exclusiveBalance = sumPostings . apostings+ -- | Is the named account considered interesting for this ledger's balance report ? isInteresting :: [Opt] -> Ledger -> AccountName -> Bool-isInteresting opts l a+isInteresting opts l a | Flat `elem` opts = isInterestingFlat opts l a+ | otherwise = isInterestingIndented opts l a++isInterestingFlat :: [Opt] -> Ledger -> AccountName -> Bool+isInterestingFlat opts l a = notempty || emptyflag+ where+ acct = ledgerAccount l a+ notempty = not $ isZeroMixedAmount $ exclusiveBalance acct+ emptyflag = Empty `elem` opts++isInterestingIndented :: [Opt] -> Ledger -> AccountName -> Bool+isInterestingIndented opts l a | numinterestingsubs==1 && not atmaxdepth = notlikesub | otherwise = notzero || emptyflag where
Hledger/Cli/Commands/Convert.hs view
@@ -1,5 +1,5 @@ {-|-Convert account data in CSV format (eg downloaded from a bank) to ledger+Convert account data in CSV format (eg downloaded from a bank) to journal format, and print it on stdout. See the manual for more details. -} @@ -8,7 +8,8 @@ import Hledger.Cli.Version (versionstr) import Hledger.Data.Types (Journal,AccountName,Transaction(..),Posting(..),PostingType(..)) import Hledger.Data.Utils (strip, spacenonewline, restofline, parseWithCtx, assertParse, assertParseEqual)-import Hledger.Data.Parse (someamount, emptyCtx, ledgeraccountname)+import Hledger.Read.Common (emptyCtx)+import Hledger.Read.Journal (someamount,ledgeraccountname) import Hledger.Data.Amount (nullmixedamt) import Safe (atDef, maximumDef) import System.IO (stderr)@@ -23,14 +24,14 @@ import Safe (readDef, readMay) import System.Directory (doesFileExist) import System.Exit (exitFailure)-import System.FilePath.Posix (takeBaseName, replaceExtension)+import System.FilePath (takeBaseName, replaceExtension) import Text.ParserCombinators.Parsec import Test.HUnit {- | A set of data definitions and account-matching patterns sufficient to-convert a particular CSV data file into meaningful ledger transactions. See above.+convert a particular CSV data file into meaningful journal transactions. See above. -} data CsvRules = CsvRules { dateField :: Maybe FieldPosition,
Hledger/Cli/Commands/Print.hs view
@@ -15,7 +15,7 @@ #endif --- | Print ledger transactions in standard format.+-- | Print journal transactions in standard format. print' :: [Opt] -> [String] -> Journal -> IO () print' opts args j = do t <- getCurrentLocalTime
Hledger/Cli/Commands/Register.hs view
@@ -19,6 +19,7 @@ import Prelude hiding ( putStr ) import System.IO.UTF8 #endif+import Text.ParserCombinators.Parsec -- | Print a register report.@@ -50,12 +51,6 @@ reportspan | empty = filterspan `orDatesFrom` dataspan | otherwise = dataspan --- | Combine two datespans, filling any unspecified dates in the first--- with dates from the second.-orDatesFrom (DateSpan a1 b1) (DateSpan a2 b2) = DateSpan a b- where a = if isJust a1 then a1 else a2- b = if isJust b1 then b1 else b2- -- | Date-sort and split a list of postings into three spans - postings matched -- by the given display expression, and the preceding and following postings. postingsMatchingDisplayExpr :: Maybe String -> [Posting] -> ([Posting],[Posting],[Posting])@@ -71,6 +66,28 @@ displayExprMatches Nothing _ = True displayExprMatches (Just d) p = (fromparse $ parsewith datedisplayexpr d) p +-- | Parse a hledger display expression, which is a simple date test like+-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.+datedisplayexpr :: GenParser Char st (Posting -> Bool)+datedisplayexpr = do+ char 'd'+ op <- compareop+ char '['+ (y,m,d) <- smartdate+ char ']'+ let date = parsedate $ printf "%04s/%02s/%02s" y m d+ test op = return $ (`op` date) . postingDate+ case op of+ "<" -> test (<)+ "<=" -> test (<=)+ "=" -> test (==)+ "==" -> test (==)+ ">=" -> test (>=)+ ">" -> test (>)+ _ -> mzero+ where+ compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]+ -- XXX confusing, refactor -- | Given a date span (representing a reporting interval) and a list of -- postings within it: aggregate the postings so there is only one per@@ -127,7 +144,7 @@ -- | Show one posting and running balance, with or without transaction info. showPostingWithBalance :: Bool -> Posting -> MixedAmount -> String-showPostingWithBalance withtxninfo p b = concatBottomPadded [txninfo ++ pstr ++ " ", bal] ++ "\n"+showPostingWithBalance withtxninfo p b = concatTopPadded [txninfo, pstr, " ", bal] ++ "\n" where ledger3ishlayout = False datedescwidth = if ledger3ishlayout then 34 else 32
Hledger/Cli/Commands/Stats.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE CPP #-} {-| -Print some statistics for the ledger.+Print some statistics for the journal. -} @@ -9,45 +9,53 @@ where import Hledger.Data import Hledger.Cli.Options+import qualified Data.Map as Map #if __GLASGOW_HASKELL__ <= 610 import Prelude hiding ( putStr ) import System.IO.UTF8 #endif-import qualified Data.Map as Map --- | Print various statistics for the ledger.+-- like Register.summarisePostings+-- | Print various statistics for the journal. stats :: [Opt] -> [String] -> Journal -> IO () stats opts args j = do today <- getCurrentDay- putStr $ showStats opts args (journalToLedger nullfilterspec j) today+ t <- getCurrentLocalTime+ let filterspec = optsToFilterSpec opts args t+ l = journalToLedger filterspec j+ reportspan = (ledgerDateSpan l) `orDatesFrom` (datespan filterspec)+ intervalspans = splitSpan (intervalFromOpts opts) reportspan+ showstats = showLedgerStats opts args l today+ s = intercalate "\n" $ map showstats intervalspans+ putStr s -showStats :: [Opt] -> [String] -> Ledger -> Day -> String-showStats _ _ l today =- heading ++ unlines (map (uncurry (printf fmt)) stats)+showLedgerStats :: [Opt] -> [String] -> Ledger -> Day -> DateSpan -> String+showLedgerStats _ _ l today span =+ unlines (map (uncurry (printf fmt)) stats) where- heading = underline $ printf "Ledger statistics as of %s" (show today) fmt = "%-" ++ show w1 ++ "s: %-" ++ show w2 ++ "s" w1 = maximum $ map (length . fst) stats w2 = maximum $ map (length . show . snd) stats stats = [- ("File", filepath $ journal l)- ,("Period", printf "%s to %s (%d days)" (start span) (end span) days)+ ("Journal file", filepath $ journal l)+ ,("Transactions span", printf "%s to %s (%d days)" (start span) (end span) days) ,("Last transaction", maybe "none" show lastdate ++ showelapsed lastelapsed) ,("Transactions", printf "%d (%0.1f per day)" tnum txnrate) ,("Transactions last 30 days", printf "%d (%0.1f per day)" tnum30 txnrate30) ,("Transactions last 7 days", printf "%d (%0.1f per day)" tnum7 txnrate7)--- ,("Payees/descriptions", show $ length $ nub $ map tdescription ts)- ,("Accounts", show $ length $ accounts l)- ,("Account tree depth", show $ maximum $ map (accountNameLevel.aname) $ accounts l)- ,("Commodities", printf "%s (%s)" (show $ length $ cs) (intercalate ", " $ sort $ map symbol cs)) + ,("Payees/descriptions", show $ length $ nub $ map tdescription ts)+ ,("Accounts", printf "%d (depth %d)" acctnum acctdepth)+ ,("Commodities", printf "%s (%s)" (show $ length cs) (intercalate ", " cs)) -- Transactions this month : %(monthtxns)s (last month in the same period: %(lastmonthtxns)s) -- Uncleared transactions : %(uncleared)s -- Days since reconciliation : %(reconcileelapsed)s -- Days since last transaction : %(recentelapsed)s ] where- ts = sortBy (comparing tdate) $ jtxns $ journal l+ ts = sortBy (comparing tdate) $ filter (spanContainsDate span . tdate) $ jtxns $ journal l+ as = nub $ map paccount $ concatMap tpostings ts+ cs = Map.keys $ canonicaliseCommodities $ nub $ map commodity $ concatMap amounts $ map pamount $ concatMap tpostings ts lastdate | null ts = Nothing | otherwise = Just $ tdate $ last ts lastelapsed = maybe Nothing (Just . diffDays today) lastdate@@ -57,7 +65,6 @@ direction | days >= 0 = "days ago" | otherwise = "days from now" tnum = length ts- span = rawdatespan l start (DateSpan (Just d) _) = show d start _ = "" end (DateSpan _ (Just d)) = show d@@ -71,5 +78,7 @@ tnum7 = length $ filter withinlast7 ts withinlast7 t = d >= addDays (-7) today && (d<=today) where d = tdate t txnrate7 = fromIntegral tnum7 / 7 :: Double- cs = Map.elems $ commodities l+ acctnum = length as+ acctdepth | null as = 0+ | otherwise = maximum $ map accountNameLevel as
Hledger/Cli/Commands/Vty.hs view
@@ -44,8 +44,8 @@ -- | The screens available within the user interface. data Screen = BalanceScreen -- ^ like hledger balance, shows accounts | RegisterScreen -- ^ like hledger register, shows transaction-postings- | PrintScreen -- ^ like hledger print, shows ledger transactions- -- | LedgerScreen -- ^ shows the raw ledger+ | PrintScreen -- ^ like hledger print, shows journal transactions+ -- | LedgerScreen -- ^ shows the raw journal deriving (Eq,Show) -- | Run the vty (curses-style) ui.
Hledger/Cli/Commands/Web.hs view
@@ -1,345 +1,442 @@-{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses #-}-{-# OPTIONS_GHC -F -pgmFtrhsx #-}+{-# LANGUAGE CPP, TypeFamilies, QuasiQuotes, TemplateHaskell #-} {-| A web-based UI. -} module Hledger.Cli.Commands.Web where-import Codec.Binary.UTF8.String (decodeString)-import Control.Applicative.Error (Failing(Success,Failure))-import Control.Concurrent-import Control.Monad.Reader (ask)-import Data.IORef (newIORef, atomicModifyIORef)-import System.Directory (getModificationTime)+import Control.Concurrent (forkIO, threadDelay)+import Control.Applicative ((<$>), (<*>))+import Data.Either+import System.FilePath ((</>)) import System.IO.Storage (withStore, putValue, getValue)-import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff)) import Text.ParserCombinators.Parsec (parse)--import Hack.Contrib.Constants (_TextHtmlUTF8)-import Hack.Contrib.Response (set_content_type)-import qualified Hack (Env, http)-import qualified Hack.Contrib.Request (inputs, params, path)-import qualified Hack.Contrib.Response (redirect)-#ifdef WEBHAPPSTACK-import System.Process (readProcess)-import Hack.Handler.Happstack (runWithConfig,ServerConf(ServerConf))-#else-import Hack.Handler.SimpleServer (run)-#endif--import Network.Loli (loli, io, get, post, html, text, public)-import Network.Loli.Type (AppUnit)-import Network.Loli.Utils (update)--import HSP hiding (Request,catch)-import qualified HSP (Request(..))+import Yesod import Hledger.Cli.Commands.Add (journalAddTransaction) import Hledger.Cli.Commands.Balance-import Hledger.Cli.Commands.Histogram import Hledger.Cli.Commands.Print import Hledger.Cli.Commands.Register-import Hledger.Data import Hledger.Cli.Options hiding (value)+import Hledger.Cli.Utils+import Hledger.Data+import Hledger.Read (journalFromPathAndString)+import Hledger.Read.Journal (someamount) #ifdef MAKE import Paths_hledger_make (getDataFileName) #else import Paths_hledger (getDataFileName) #endif-import Hledger.Cli.Utils (openBrowserOn) -tcpport = 5000 :: Int-homeurl = printf "http://localhost:%d/" tcpport-browserdelay = 100000 -- microseconds+defhost = "localhost"+defport = 5000+defbaseurl = printf "http://%s:%d" defhost defport :: String+browserstartdelay = 100000 -- microseconds+hledgerurl = "http://hledger.org"+manualurl = hledgerurl++"/MANUAL.html" web :: [Opt] -> [String] -> Journal -> IO () web opts args j = do- unless (Debug `elem` opts) $ forkIO browser >> return ()- server opts args j+ let baseurl = fromMaybe defbaseurl $ baseUrlFromOpts opts+ port = fromMaybe defport $ portFromOpts opts+ unless (Debug `elem` opts) $ forkIO (browser baseurl) >> return ()+ server baseurl port opts args j -browser :: IO ()-browser = putStrLn "starting web browser" >> threadDelay browserdelay >> openBrowserOn homeurl >> return ()+browser :: String -> IO ()+browser baseurl = do+ putStrLn "starting web browser"+ threadDelay browserstartdelay+ openBrowserOn baseurl+ return () -server :: [Opt] -> [String] -> Journal -> IO ()-server opts args j =- -- server initialisation- withStore "hledger" $ do -- IO ()- printf "starting web server on port %d\n" tcpport- t <- getCurrentLocalTime- webfiles <- getDataFileName "web"- putValue "hledger" "journal" j-#ifdef WEBHAPPSTACK- hostname <- readProcess "hostname" [] "" `catch` \_ -> return "hostname"- runWithConfig (ServerConf tcpport hostname) $ -- (Env -> IO Response) -> IO ()-#else- run tcpport $ -- (Env -> IO Response) -> IO ()-#endif- \env -> do -- IO Response- -- general request handler- let opts' = opts ++ [Period $ unwords $ map decodeString $ reqParamUtf8 env "p"]- args' = args ++ map decodeString (reqParamUtf8 env "a")- j' <- fromJust `fmap` getValue "hledger" "journal"- j'' <- journalReloadIfChanged opts' args' j'- -- declare path-specific request handlers- let command :: [String] -> ([Opt] -> FilterSpec -> Journal -> String) -> AppUnit- command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) j''- (loli $ -- State Loli () -> (Env -> IO Response)- do- get "/balance" $ command [] showBalanceReport -- String -> ReaderT Env (StateT Response IO) () -> State Loli ()- get "/register" $ command [] showRegisterReport- get "/histogram" $ command [] showHistogram- get "/transactions" $ ledgerpage [] j'' (showTransactions (optsToFilterSpec opts' args' t))- post "/transactions" $ handleAddform j''- get "/env" $ getenv >>= (text . show)- get "/params" $ getenv >>= (text . show . Hack.Contrib.Request.params)- get "/inputs" $ getenv >>= (text . show . Hack.Contrib.Request.inputs)- public (Just webfiles) ["/style.css"]- get "/" $ redirect ("transactions") Nothing- ) env+server :: String -> Int -> [Opt] -> [String] -> Journal -> IO ()+server baseurl port opts args j = do+ printf "starting web server on port %d with base url %s\n" port baseurl+ fp <- getDataFileName "web"+ let app = HledgerWebApp{+ appOpts=opts+ ,appArgs=args+ ,appJournal=j+ ,appWebdir=fp+ ,appRoot=baseurl+ }+ withStore "hledger" $ do+ putValue "hledger" "journal" j+ basicHandler port app -getenv = ask-response = update-redirect u c = response $ Hack.Contrib.Response.redirect u c+data HledgerWebApp = HledgerWebApp {+ appOpts::[Opt]+ ,appArgs::[String]+ ,appJournal::Journal+ ,appWebdir::FilePath+ ,appRoot::String+ } -reqParamUtf8 :: Hack.Env -> String -> [String]-reqParamUtf8 env p = map snd $ filter ((==p).fst) $ Hack.Contrib.Request.params env+mkYesod "HledgerWebApp" [$parseRoutes|+/ IndexPage GET+/style.css StyleCss GET+/journal JournalPage GET POST+/edit EditPage GET POST+/register RegisterPage GET+/balance BalancePage GET+|] -journalReloadIfChanged :: [Opt] -> [String] -> Journal -> IO Journal-journalReloadIfChanged opts _ j@Journal{filepath=f,filereadtime=tread} = do- tmod <- journalFileModifiedTime j- let newer = diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)- -- when (Debug `elem` opts) $ printf "checking file, last modified %s, last read %s, %s\n" (show tmod) (show tread) (show newer)- if newer- then do- when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" f- reload j- else return j+instance Yesod HledgerWebApp where approot = appRoot -journalFileModifiedTime :: Journal -> IO ClockTime-journalFileModifiedTime Journal{filepath=f}- | null f = getClockTime- | otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime+getIndexPage :: Handler HledgerWebApp ()+getIndexPage = redirect RedirectTemporary JournalPage -reload :: Journal -> IO Journal-reload Journal{filepath=f} = do- j' <- readJournal f- putValue "hledger" "journal" j'- return j'- -ledgerpage :: [String] -> Journal -> (Journal -> String) -> AppUnit-ledgerpage msgs j f = do- env <- getenv- j' <- io $ journalReloadIfChanged [] [] j- hsp msgs $ const <div><% addform env %><pre><% f j' %></pre></div>+getStyleCss :: Handler HledgerWebApp ()+getStyleCss = do+ app <- getYesod+ let dir = appWebdir app+ sendFile "text/css" $ dir </> "style.css" --- | A loli directive to serve a string in pre tags within the hledger web--- layout.-string :: [String] -> String -> AppUnit-string msgs s = hsp msgs $ const <pre><% s %></pre>+getJournalPage :: Handler HledgerWebApp RepHtml+getJournalPage = withLatestJournalRender (const showTransactions) --- | A loli directive to serve a hsp template wrapped in the hledger web--- layout. The hack environment is passed in to every hsp template as an--- argument, since I don't see how to get it within the hsp monad.--- A list of messages is also passed, eg for form errors.-hsp :: [String] -> (Hack.Env -> HSP XML) -> AppUnit-hsp msgs f = do- env <- getenv- let contenthsp = f env- pagehsp = hledgerpage env msgs title contenthsp- html =<< (io $ do- hspenv <- hackEnvToHspEnv env- (_,xml) <- runHSP html4Strict pagehsp hspenv- return $ addDoctype $ renderAsHTML xml)- response $ set_content_type _TextHtmlUTF8- where- title = ""- addDoctype = ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" ++)- hackEnvToHspEnv :: Hack.Env -> IO HSPEnv- hackEnvToHspEnv env = do- x <- newIORef 0- let req = HSP.Request (reqParamUtf8 env) (Hack.http env)- num = NumberGen (atomicModifyIORef x (\a -> (a+1,a)))- return $ HSPEnv req num+getRegisterPage :: Handler HledgerWebApp RepHtml+getRegisterPage = withLatestJournalRender showRegisterReport --- htmlToHsp :: Html -> HSP XML--- htmlToHsp h = return $ cdata $ showHtml h+getBalancePage :: Handler HledgerWebApp RepHtml+getBalancePage = withLatestJournalRender showBalanceReport --- views+withLatestJournalRender :: ([Opt] -> FilterSpec -> Journal -> String) -> Handler HledgerWebApp RepHtml+withLatestJournalRender reportfn = do+ app <- getYesod+ t <- liftIO $ getCurrentLocalTime+ a <- fromMaybe "" <$> lookupGetParam "a"+ p <- fromMaybe "" <$> lookupGetParam "p"+ let opts = appOpts app ++ [Period p]+ args = appArgs app ++ [a]+ fspec = optsToFilterSpec opts args t+ -- reload journal if changed, displaying any error as a message+ j <- liftIO $ fromJust `fmap` getValue "hledger" "journal"+ (jE, changed) <- liftIO $ journalReloadIfChanged opts j+ let (j', err) = either (\e -> (j,e)) (\j -> (j,"")) jE+ when (changed && null err) $ liftIO $ putValue "hledger" "journal" j'+ if (changed && not (null err)) then setMessage $ string "error while reading"+ else return ()+ -- run the specified report using this request's params+ let s = reportfn opts fspec j'+ -- render the standard template+ msg' <- getMessage+ -- XXX work around a bug, can't get the message we set above+ let msg = if null err then msg' else Just $ string $ printf "Error while reading %s" (filepath j')+ Just here <- getCurrentRoute+ hamletToRepHtml $ template here msg a p "hledger" s -hledgerpage :: Hack.Env -> [String] -> String -> HSP XML -> HSP XML-hledgerpage env msgs title content =- <html>- <head>- <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />- <link rel="stylesheet" type="text/css" href="/style.css" media="all" />- <title><% title %></title>- </head>- <body>- <% navbar env %>- <div id="messages"><% intercalate ", " msgs %></div>- <div id="content"><% content %></div>- </body>- </html>+template :: HledgerWebAppRoute -> Maybe (Html ()) -> String -> String+ -> String -> String -> Hamlet HledgerWebAppRoute+template here msg a p title content = [$hamlet|+!!!+%html+ %head+ %title $string.title$+ %meta!http-equiv=Content-Type!content=$string.metacontent$+ %link!rel=stylesheet!type=text/css!href=@stylesheet@!media=all+ %body+ ^navbar'^+ #messages $m$+ ^addform'^+ #content+ %pre $string.content$+|]+ where m = fromMaybe (string "") msg+ navbar' = navbar here a p+ addform' | here == JournalPage = addform+ | otherwise = nulltemplate+ stylesheet = StyleCss+ metacontent = "text/html; charset=utf-8" -navbar :: Hack.Env -> HSP XML-navbar env =- <div id="navbar">- <a href="http://hledger.org" id="hledgerorglink">hledger.org</a>- <% navlinks env %>- <% searchform env %>- <a href="http://hledger.org/MANUAL.html" id="helplink">help</a>- </div>+nulltemplate = [$hamlet||] -getParamOrNull p = (decodeString . fromMaybe "") `fmap` getParam p+navbar :: HledgerWebAppRoute -> String -> String -> Hamlet HledgerWebAppRoute+navbar here a p = [$hamlet|+ #navbar+ %a.toprightlink!href=$string.hledgerurl$ hledger.org+ \ $+ %a.toprightlink!href=$string.manualurl$ manual+ \ $+ ^navlinks'^+ ^searchform'^+|]+ where navlinks' = navlinks here a p+ searchform' = searchform here a p -navlinks :: Hack.Env -> HSP XML-navlinks _ = do- a <- getParamOrNull "a"- p <- getParamOrNull "p"- let addparams=(++(printf "?a=%s&p=%s" a p))- link s = <a href=(addparams s) class="navlink"><% s %></a>- <div id="navlinks">- <% link "transactions" %> |- <% link "register" %> |- <% link "balance" %>- </div>+navlinks :: HledgerWebAppRoute -> String -> String -> Hamlet HledgerWebAppRoute+navlinks here a p = [$hamlet|+ #navlinks+ ^journallink^ $+ (^editlink^) $+ | ^registerlink^ $+ | ^balancelink^ $+|]+ where+ journallink = navlink here "journal" JournalPage+ editlink = navlink here "edit" EditPage+ registerlink = navlink here "register" RegisterPage+ balancelink = navlink here "balance" BalancePage+ navlink here s dest = [$hamlet|%a.$style$!href=@?u@ $string.s$|]+ where u = (dest, concat [(if null a then [] else [("a", a)])+ ,(if null p then [] else [("p", p)])])+ style | here == dest = string "navlinkcurrent"+ | otherwise = string "navlink" -searchform :: Hack.Env -> HSP XML-searchform env = do- a <- getParamOrNull "a"- p <- getParamOrNull "p"- let resetlink | null a && null p = <span></span>- | otherwise = <span id="resetlink"><% nbsp %><a href=u>reset</a></span>- where u = dropWhile (=='/') $ Hack.Contrib.Request.path env- <form action="" id="searchform">- <% nbsp %>search for:<% nbsp %><input name="a" size="20" value=a- /><% help "filter-patterns"- %><% nbsp %><% nbsp %>in reporting period:<% nbsp %><input name="p" size="20" value=p- /><% help "period-expressions"- %><input type="submit" name="submit" value="filter" style="display:none" />- <% resetlink %>- </form>+searchform :: HledgerWebAppRoute -> String -> String -> Hamlet HledgerWebAppRoute+searchform here a p = [$hamlet|+ %form#searchform!method=GET+ filter by: $+ %input!name=a!size=20!value=$string.a$+ ^ahelp^ $+ in period: $+ %input!name=p!size=20!value=$string.p$+ ^phelp^ $+ %input!type=submit!value=filter+ ^resetlink^+|]+ where+ ahelp = helplink "filter-patterns" "?"+ phelp = helplink "period-expressions" "?"+ resetlink+ | null a && null p = nulltemplate+ | otherwise = [$hamlet|%span#resetlink $+ %a!href=@here@ reset|] -addform :: Hack.Env -> HSP XML-addform env = do- today <- io $ liftM showDate $ getCurrentDay- let inputs = Hack.Contrib.Request.inputs env- date = decodeString $ fromMaybe today $ lookup "date" inputs- desc = decodeString $ fromMaybe "" $ lookup "desc" inputs- <div>- <div id="addform">- <form action="" method="POST">- <table border="0">- <tr>- <td>- Date: <input size="15" name="date" value=date /><% help "dates" %><% nbsp %>- Description: <input size="35" name="desc" value=desc /><% nbsp %>- </td>- </tr>- <% transactionfields 1 env %>- <% transactionfields 2 env %>- <tr id="addbuttonrow"><td><input type="submit" value="add transaction" - /><% help "file-format" %></td></tr>- </table>- </form>- </div>- <br clear="all" />- </div>+helplink topic label = [$hamlet|%a!href=$string.u$ $string.label$|]+ where u = manualurl ++ if null topic then "" else '#':topic -help :: String -> HSP XML-help topic = <a href=u>?</a>- where u = printf "http://hledger.org/MANUAL.html%s" l :: String- l | null topic = ""- | otherwise = '#':topic+addform :: Hamlet HledgerWebAppRoute+addform = [$hamlet|+ %form!method=POST+ %table.form#addform!cellpadding=0!cellspacing=0!border=0+ %tr.formheading+ %td!colspan=4+ %span#formheading Add a transaction:+ %tr+ %td!colspan=4+ %table!cellpadding=0!cellspacing=0!border=0+ %tr#descriptionrow+ %td+ Date:+ %td+ %input!size=15!name=date!value=$string.date$+ %td+ Description:+ %td+ %input!size=35!name=description!value=$string.desc$+ %tr.helprow+ %td+ %td+ #help $string.datehelp$ ^datehelplink^ $+ %td+ %td+ #help $string.deschelp$+ ^transactionfields1^+ ^transactionfields2^+ %tr#addbuttonrow+ %td!colspan=4+ %input!type=submit!value=$string.addlabel$+|]+ where+ datehelplink = helplink "dates" "..."+ datehelp = "eg: 7/20, 2010/1/1, "+ deschelp = "eg: supermarket (optional)"+ addlabel = "add transaction"+ date = "today"+ desc = ""+ transactionfields1 = transactionfields 1+ transactionfields2 = transactionfields 2 -transactionfields :: Int -> Hack.Env -> HSP XML-transactionfields n env = do- let inputs = Hack.Contrib.Request.inputs env- acct = decodeString $ fromMaybe "" $ lookup acctvar inputs- amt = decodeString $ fromMaybe "" $ lookup amtvar inputs- <tr>- <td>- <% nbsp %><% nbsp %>- Account: <input size="35" name=acctvar value=acct /><% nbsp %>- Amount: <input size="15" name=amtvar value=amt /><% nbsp %>- </td>- </tr>- where- numbered = (++ show n)- acctvar = numbered "acct"- amtvar = numbered "amt"+-- transactionfields :: Int -> Hamlet String+transactionfields n = [$hamlet|+ %tr#postingrow+ %td!align=right+ $string.label$:+ %td+ %input!size=35!name=$string.acctvar$!value=$string.acct$+ ^amtfield^+ %tr.helprow+ %td+ %td+ #help $string.accthelp$+ %td+ %td+ #help $string.amthelp$+|]+ where+ label | n == 1 = "To account"+ | otherwise = "From account"+ accthelp | n == 1 = "eg: expenses:food"+ | otherwise = "eg: assets:bank:checking"+ amtfield | n == 1 = [$hamlet|+ %td+ Amount:+ %td+ %input!size=15!name=$string.amtvar$!value=$string.amt$+ |]+ | otherwise = nulltemplate+ amthelp | n == 1 = "eg: 5, $6, €7.01"+ | otherwise = ""+ acct = ""+ amt = ""+ numbered = (++ show n)+ acctvar = numbered "accountname"+ amtvar = numbered "amount" -handleAddform :: Journal -> AppUnit-handleAddform j = do- env <- getenv- d <- io getCurrentDay- t <- io getCurrentLocalTime- handle t $ validate env d- where- validate :: Hack.Env -> Day -> Failing Transaction- validate env today =- let inputs = Hack.Contrib.Request.inputs env- date = decodeString $ fromMaybe "today" $ lookup "date" inputs- desc = decodeString $ fromMaybe "" $ lookup "desc" inputs- acct1 = decodeString $ fromMaybe "" $ lookup "acct1" inputs- amt1 = decodeString $ fromMaybe "" $ lookup "amt1" inputs- acct2 = decodeString $ fromMaybe "" $ lookup "acct2" inputs- amt2 = decodeString $ fromMaybe "" $ lookup "amt2" inputs- validateDate "" = ["missing date"]- validateDate _ = []- validateDesc "" = ["missing description"]- validateDesc _ = []- validateAcct1 "" = ["missing account 1"]- validateAcct1 _ = []- validateAmt1 "" = ["missing amount 1"]- validateAmt1 _ = []- validateAcct2 "" = ["missing account 2"]- validateAcct2 _ = []- validateAmt2 _ = []- amt1' = either (const missingamt) id $ parse someamount "" amt1- amt2' = either (const missingamt) id $ parse someamount "" amt2- (date', dateparseerr) = case fixSmartDateStrEither today date of- Right d -> (d, [])- Left e -> ("1900/01/01", [showDateParseError e])- t = Transaction {- tdate = parsedate date' -- date' must be parseable- ,teffectivedate=Nothing- ,tstatus=False- ,tcode=""- ,tdescription=desc- ,tcomment=""- ,tpostings=[- Posting False acct1 amt1' "" RegularPosting (Just t')- ,Posting False acct2 amt2' "" RegularPosting (Just t')- ]- ,tpreceding_comment_lines=""- }- (t', balanceerr) = case balanceTransaction t of- Right t'' -> (t'', [])- Left e -> (t, [head $ lines e]) -- show just the error not the transaction- errs = concat [- validateDate date- ,dateparseerr- ,validateDesc desc- ,validateAcct1 acct1- ,validateAmt1 amt1- ,validateAcct2 acct2- ,validateAmt2 amt2- ,balanceerr- ]- in- case null errs of- False -> Failure errs- True -> Success t'+postJournalPage :: Handler HledgerWebApp RepPlain+postJournalPage = do+ today <- liftIO getCurrentDay+ -- get form input values. M means a Maybe value.+ (dateM, descM, acct1M, amt1M, acct2M, amt2M) <- runFormPost'+ $ (,,,,,)+ <$> maybeStringInput "date"+ <*> maybeStringInput "description"+ <*> maybeStringInput "accountname1"+ <*> maybeStringInput "amount1"+ <*> maybeStringInput "accountname2"+ <*> maybeStringInput "amount2"+ -- supply defaults and parse date and amounts, or get errors.+ let dateE = maybe (Left "date required") (either (\e -> Left $ showDateParseError e) Right . fixSmartDateStrEither today) dateM+ descE = Right $ fromMaybe "" descM+ acct1E = maybe (Left "to account required") Right acct1M+ acct2E = maybe (Left "from account required") Right acct2M+ amt1E = maybe (Left "amount required") (either (const $ Left "could not parse amount") Right . parse someamount "") amt1M+ amt2E = maybe (Right missingamt) (either (const $ Left "could not parse amount") Right . parse someamount "") amt2M+ strEs = [dateE, descE, acct1E, acct2E]+ amtEs = [amt1E, amt2E]+ [date,desc,acct1,acct2] = rights strEs+ [amt1,amt2] = rights amtEs+ errs = lefts strEs ++ lefts amtEs+ -- if no errors so far, generate a transaction and balance it or get the error.+ tE | not $ null errs = Left errs+ | otherwise = either (\e -> Left ["unbalanced postings: " ++ (head $ lines e)]) Right+ (balanceTransaction $ nulltransaction {+ tdate=parsedate date+ ,teffectivedate=Nothing+ ,tstatus=False+ ,tcode=""+ ,tdescription=desc+ ,tcomment=""+ ,tpostings=[+ Posting False acct1 amt1 "" RegularPosting Nothing+ ,Posting False acct2 amt2 "" RegularPosting Nothing+ ]+ ,tpreceding_comment_lines=""+ })+ -- display errors or add transaction+ case tE of+ Left errs -> do+ -- save current form values in session+ setMessage $ string $ intercalate "; " errs+ redirect RedirectTemporary JournalPage - handle :: LocalTime -> Failing Transaction -> AppUnit- handle _ (Failure errs) = hsp errs addform- handle ti (Success t) = do- io $ journalAddTransaction j t >> reload j- ledgerpage [msg] j (showTransactions (optsToFilterSpec [] [] ti))- where msg = printf "Added transaction:\n%s" (show t)+ Right t -> do+ let t' = txnTieKnot t -- XXX move into balanceTransaction+ j <- liftIO $ fromJust `fmap` getValue "hledger" "journal"+ liftIO $ journalAddTransaction j t'+ setMessage $ string $ printf "Added transaction:\n%s" (show t')+ redirect RedirectTemporary JournalPage -nbsp :: XML-nbsp = cdata " "+getEditPage :: Handler HledgerWebApp RepHtml+getEditPage = do+ -- app <- getYesod+ -- t <- liftIO $ getCurrentLocalTime+ a <- fromMaybe "" <$> lookupGetParam "a"+ p <- fromMaybe "" <$> lookupGetParam "p"+ -- opts = appOpts app ++ [Period p]+ -- args = appArgs app ++ [a]+ -- fspec = optsToFilterSpec opts args t+ -- reload journal's text, without parsing, if changed+ j <- liftIO $ fromJust `fmap` getValue "hledger" "journal"+ changed <- liftIO $ journalFileIsNewer j+ -- XXX readFile may throw an error+ s <- liftIO $ if changed then readFile (filepath j) else return (jtext j)+ -- render the page+ msg <- getMessage+ Just here <- getCurrentRoute+ hamletToRepHtml $ template' here msg a p "hledger" s++template' here msg a p title content = [$hamlet|+!!!+%html+ %head+ %title $string.title$+ %meta!http-equiv=Content-Type!content=$string.metacontent$+ %link!rel=stylesheet!type=text/css!href=@stylesheet@!media=all+ %body+ ^navbar'^+ #messages $m$+ ^editform'^+|]+ where m = fromMaybe (string "") msg+ navbar' = navbar here a p+ stylesheet = StyleCss+ metacontent = "text/html; charset=utf-8"+ editform' = editform content++editform :: String -> Hamlet HledgerWebAppRoute+editform t = [$hamlet|+ %form!method=POST+ %table.form#editform!cellpadding=0!cellspacing=0!border=0+ %tr.formheading+ %td!colspan=2+ %span!style=float:right; ^formhelp^+ %span#formheading Edit journal:+ %tr+ %td!colspan=2+ %textarea!name=text!rows=30!cols=80+ $string.t$+ %tr#addbuttonrow+ %td+ %a!href=@JournalPage@ cancel+ %td!align=right+ %input!type=submit!value=$string.submitlabel$+ %tr.helprow+ %td+ %td!align=right+ #help $string.edithelp$+|]+ where+ submitlabel = "save journal"+ formhelp = helplink "file-format" "file format help"+ edithelp = "Are you sure ? All previous data will be replaced"++postEditPage :: Handler HledgerWebApp RepPlain+postEditPage = do+ -- get form input values, or basic validation errors. E means an Either value.+ textM <- runFormPost' $ maybeStringInput "text"+ let textE = maybe (Left "No value provided") Right textM+ -- display errors or add transaction+ case textE of+ Left errs -> do+ -- XXX should save current form values in session+ setMessage $ string errs+ redirect RedirectTemporary JournalPage++ Right t' -> do+ -- try to avoid unnecessary backups or saving invalid data+ j <- liftIO $ fromJust `fmap` getValue "hledger" "journal"+ filechanged' <- liftIO $ journalFileIsNewer j+ let f = filepath j+ told = jtext j+ tnew = filter (/= '\r') t'+ changed = tnew /= told || filechanged'+-- changed <- liftIO $ writeFileWithBackupIfChanged f t''+ if not changed+ then do+ setMessage $ string $ "No change"+ redirect RedirectTemporary EditPage+ else do+ jE <- liftIO $ journalFromPathAndString Nothing f tnew+ either+ (\e -> do+ setMessage $ string e+ redirect RedirectTemporary EditPage)+ (const $ do+ liftIO $ writeFileWithBackup f tnew+ setMessage $ string $ printf "Saved journal %s\n" (show f)+ redirect RedirectTemporary JournalPage)+ jE+
+ Hledger/Cli/Commands/Web610.hs view
@@ -0,0 +1,316 @@+{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses #-}+{-# OPTIONS_GHC -F -pgmFtrhsx #-}+{-| +A web-based UI.+-}++module Hledger.Cli.Commands.Web610+where+import Codec.Binary.UTF8.String (decodeString)+import Control.Applicative.Error (Failing(Success,Failure))+import Control.Concurrent+import Control.Monad.Reader (ask)+import Data.IORef (newIORef, atomicModifyIORef)+import System.IO.Storage (withStore, putValue, getValue)+import Text.ParserCombinators.Parsec (parse)++import Hack.Contrib.Constants (_TextHtmlUTF8)+import Hack.Contrib.Response (set_content_type)+import qualified Hack (Env, http)+import qualified Hack.Contrib.Request (inputs, params, path)+import qualified Hack.Contrib.Response (redirect)+import Hack.Handler.SimpleServer (run)++import Network.Loli (loli, io, get, post, html, text, public)+import Network.Loli.Type (AppUnit)+import Network.Loli.Utils (update)++import HSP hiding (Request,catch)+import qualified HSP (Request(..))++import Hledger.Cli.Commands.Add (journalAddTransaction)+import Hledger.Cli.Commands.Balance+import Hledger.Cli.Commands.Histogram+import Hledger.Cli.Commands.Print+import Hledger.Cli.Commands.Register+import Hledger.Data+import Hledger.Read.Journal (someamount)+import Hledger.Cli.Options hiding (value)+#ifdef MAKE+import Paths_hledger_make (getDataFileName)+#else+import Paths_hledger (getDataFileName)+#endif+import Hledger.Cli.Utils+++tcpport = 5000 :: Int+homeurl = printf "http://localhost:%d/" tcpport+browserdelay = 100000 -- microseconds++web :: [Opt] -> [String] -> Journal -> IO ()+web opts args j = do+ unless (Debug `elem` opts) $ forkIO browser >> return ()+ server opts args j++browser :: IO ()+browser = putStrLn "starting web browser" >> threadDelay browserdelay >> openBrowserOn homeurl >> return ()++server :: [Opt] -> [String] -> Journal -> IO ()+server opts args j =+ -- server initialisation+ withStore "hledger" $ do -- IO ()+ printf "starting web server on port %d\n" tcpport+ t <- getCurrentLocalTime+ webfiles <- getDataFileName "web"+ putValue "hledger" "journal" j+ run tcpport $ -- (Env -> IO Response) -> IO ()+ \env -> do -- IO Response+ -- general request handler+ let opts' = opts ++ [Period $ unwords $ map decodeString $ reqParamUtf8 env "p"]+ args' = args ++ map decodeString (reqParamUtf8 env "a")+ j' <- fromJust `fmap` getValue "hledger" "journal"+ (jE, changed) <- io $ journalReloadIfChanged opts j'+ let (j''', err) = either (\e -> (j',e)) (\j'' -> (j'',"")) jE+ when (changed && null err) $ putValue "hledger" "journal" j'''+ when (changed && not (null err)) $ printf "error while reading %s\n" (filepath j')+ -- declare path-specific request handlers+ let command :: [String] -> ([Opt] -> FilterSpec -> Journal -> String) -> AppUnit+ command msgs f = string msgs $ f opts' (optsToFilterSpec opts' args' t) j'''+ (loli $ -- State Loli () -> (Env -> IO Response)+ do+ get "/balance" $ command [] showBalanceReport -- String -> ReaderT Env (StateT Response IO) () -> State Loli ()+ get "/register" $ command [] showRegisterReport+ get "/histogram" $ command [] showHistogram+ get "/transactions" $ journalpage [] j''' (showTransactions (optsToFilterSpec opts' args' t))+ post "/transactions" $ handleAddform j'''+ get "/env" $ getenv >>= (text . show)+ get "/params" $ getenv >>= (text . show . Hack.Contrib.Request.params)+ get "/inputs" $ getenv >>= (text . show . Hack.Contrib.Request.inputs)+ public (Just webfiles) ["/style.css"]+ get "/" $ redirect ("transactions") Nothing+ ) env++getenv = ask+response = update+redirect u c = response $ Hack.Contrib.Response.redirect u c++reqParamUtf8 :: Hack.Env -> String -> [String]+reqParamUtf8 env p = map snd $ filter ((==p).fst) $ Hack.Contrib.Request.params env++journalpage :: [String] -> Journal -> (Journal -> String) -> AppUnit+journalpage msgs j f = do+ env <- getenv+ (jE, _) <- io $ journalReloadIfChanged [] j+ let (j'', _) = either (\e -> (j,e)) (\j' -> (j',"")) jE+ hsp msgs $ const <div><% addform env %><pre><% f j'' %></pre></div>++-- | A loli directive to serve a string in pre tags within the hledger web+-- layout.+string :: [String] -> String -> AppUnit+string msgs s = hsp msgs $ const <pre><% s %></pre>++-- | A loli directive to serve a hsp template wrapped in the hledger web+-- layout. The hack environment is passed in to every hsp template as an+-- argument, since I don't see how to get it within the hsp monad.+-- A list of messages is also passed, eg for form errors.+hsp :: [String] -> (Hack.Env -> HSP XML) -> AppUnit+hsp msgs f = do+ env <- getenv+ let contenthsp = f env+ pagehsp = hledgerpage env msgs title contenthsp+ html =<< (io $ do+ hspenv <- hackEnvToHspEnv env+ (_,xml) <- runHSP html4Strict pagehsp hspenv+ return $ addDoctype $ renderAsHTML xml)+ response $ set_content_type _TextHtmlUTF8+ where+ title = ""+ addDoctype = ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" ++)+ hackEnvToHspEnv :: Hack.Env -> IO HSPEnv+ hackEnvToHspEnv env = do+ x <- newIORef 0+ let req = HSP.Request (reqParamUtf8 env) (Hack.http env)+ num = NumberGen (atomicModifyIORef x (\a -> (a+1,a)))+ return $ HSPEnv req num++-- htmlToHsp :: Html -> HSP XML+-- htmlToHsp h = return $ cdata $ showHtml h++-- views++hledgerpage :: Hack.Env -> [String] -> String -> HSP XML -> HSP XML+hledgerpage env msgs title content =+ <html>+ <head>+ <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />+ <link rel="stylesheet" type="text/css" href="/style.css" media="all" />+ <title><% title %></title>+ </head>+ <body>+ <% navbar env %>+ <div id="messages"><% intercalate ", " msgs %></div>+ <div id="content"><% content %></div>+ </body>+ </html>++navbar :: Hack.Env -> HSP XML+navbar env =+ <div id="navbar">+ <a href="http://hledger.org" id="hledgerorglink">hledger.org</a>+ <% navlinks env %>+ <% searchform env %>+ <a href="http://hledger.org/MANUAL.html" id="helplink">help</a>+ </div>++getParamOrNull p = (decodeString . fromMaybe "") `fmap` getParam p++navlinks :: Hack.Env -> HSP XML+navlinks _ = do+ a <- getParamOrNull "a"+ p <- getParamOrNull "p"+ let addparams=(++(printf "?a=%s&p=%s" a p))+ link s = <a href=(addparams s) class="navlink"><% s %></a>+ <div id="navlinks">+ <% link "transactions" %> |+ <% link "register" %> |+ <% link "balance" %>+ </div>++searchform :: Hack.Env -> HSP XML+searchform env = do+ a <- getParamOrNull "a"+ p <- getParamOrNull "p"+ let resetlink | null a && null p = <span></span>+ | otherwise = <span id="resetlink"><% nbsp %><a href=u>reset</a></span>+ where u = dropWhile (=='/') $ Hack.Contrib.Request.path env+ <form action="" id="searchform">+ <% nbsp %>search for:<% nbsp %><input name="a" size="20" value=a+ /><% help "filter-patterns"+ %><% nbsp %><% nbsp %>in reporting period:<% nbsp %><input name="p" size="20" value=p+ /><% help "period-expressions"+ %><input type="submit" name="submit" value="filter" style="display:none" />+ <% resetlink %>+ </form>++addform :: Hack.Env -> HSP XML+addform env = do+ today <- io $ liftM showDate $ getCurrentDay+ let inputs = Hack.Contrib.Request.inputs env+ date = decodeString $ fromMaybe today $ lookup "date" inputs+ desc = decodeString $ fromMaybe "" $ lookup "desc" inputs+ <div>+ <div id="addform">+ <form action="" method="POST">+ <table border="0">+ <tr>+ <td>+ Date: <input size="15" name="date" value=date /><% help "dates" %><% nbsp %>+ Description: <input size="35" name="desc" value=desc /><% nbsp %>+ </td>+ </tr>+ <% transactionfields 1 env %>+ <% transactionfields 2 env %>+ <tr id="addbuttonrow"><td><input type="submit" value="add transaction" + /><% help "file-format" %></td></tr>+ </table>+ </form>+ </div>+ <br clear="all" />+ </div>++help :: String -> HSP XML+help topic = <a href=u>?</a>+ where u = printf "http://hledger.org/MANUAL.html%s" l :: String+ l | null topic = ""+ | otherwise = '#':topic++transactionfields :: Int -> Hack.Env -> HSP XML+transactionfields n env = do+ let inputs = Hack.Contrib.Request.inputs env+ acct = decodeString $ fromMaybe "" $ lookup acctvar inputs+ amt = decodeString $ fromMaybe "" $ lookup amtvar inputs+ <tr>+ <td>+ <% nbsp %><% nbsp %>+ Account: <input size="35" name=acctvar value=acct /><% nbsp %>+ Amount: <input size="15" name=amtvar value=amt /><% nbsp %>+ </td>+ </tr>+ where+ numbered = (++ show n)+ acctvar = numbered "acct"+ amtvar = numbered "amt"++handleAddform :: Journal -> AppUnit+handleAddform j = do+ env <- getenv+ d <- io getCurrentDay+ t <- io getCurrentLocalTime+ handle t $ validate env d+ where+ validate :: Hack.Env -> Day -> Failing Transaction+ validate env today =+ let inputs = Hack.Contrib.Request.inputs env+ date = decodeString $ fromMaybe "today" $ lookup "date" inputs+ desc = decodeString $ fromMaybe "" $ lookup "desc" inputs+ acct1 = decodeString $ fromMaybe "" $ lookup "acct1" inputs+ amt1 = decodeString $ fromMaybe "" $ lookup "amt1" inputs+ acct2 = decodeString $ fromMaybe "" $ lookup "acct2" inputs+ amt2 = decodeString $ fromMaybe "" $ lookup "amt2" inputs+ validateDate "" = ["missing date"]+ validateDate _ = []+ validateDesc "" = ["missing description"]+ validateDesc _ = []+ validateAcct1 "" = ["missing account 1"]+ validateAcct1 _ = []+ validateAmt1 "" = ["missing amount 1"]+ validateAmt1 _ = []+ validateAcct2 "" = ["missing account 2"]+ validateAcct2 _ = []+ validateAmt2 _ = []+ amt1' = either (const missingamt) id $ parse someamount "" amt1+ amt2' = either (const missingamt) id $ parse someamount "" amt2+ (date', dateparseerr) = case fixSmartDateStrEither today date of+ Right d -> (d, [])+ Left e -> ("1900/01/01", [showDateParseError e])+ t = Transaction {+ tdate = parsedate date' -- date' must be parseable+ ,teffectivedate=Nothing+ ,tstatus=False+ ,tcode=""+ ,tdescription=desc+ ,tcomment=""+ ,tpostings=[+ Posting False acct1 amt1' "" RegularPosting (Just t')+ ,Posting False acct2 amt2' "" RegularPosting (Just t')+ ]+ ,tpreceding_comment_lines=""+ }+ (t', balanceerr) = case balanceTransaction t of+ Right t'' -> (t'', [])+ Left e -> (t, [head $ lines e]) -- show just the error not the transaction+ errs = concat [+ validateDate date+ ,dateparseerr+ ,validateDesc desc+ ,validateAcct1 acct1+ ,validateAmt1 amt1+ ,validateAcct2 acct2+ ,validateAmt2 amt2+ ,balanceerr+ ]+ in+ case null errs of+ False -> Failure errs+ True -> Success t'++ handle :: LocalTime -> Failing Transaction -> AppUnit+ handle _ (Failure errs) = hsp errs addform+ handle ti (Success t) = do+ io $ journalAddTransaction j t >>= journalReload+ journalpage [msg] j (showTransactions (optsToFilterSpec [] [] ti))+ where msg = printf "Added transaction:\n%s" (show t)++nbsp :: XML+nbsp = cdata " "
Hledger/Cli/Main.hs view
@@ -20,7 +20,7 @@ or ghci: > $ ghci hledger-> > j <- readJournal "data/sample.journal"+> > j <- readJournalFile "data/sample.journal" > > register [] ["income","expenses"] j > 2008/01/01 income income:salary $-1 $-1 > 2008/06/01 gift income:gifts $-1 $-2@@ -39,6 +39,9 @@ -} module Hledger.Cli.Main where+#if defined(WEB) || defined(WEB610)+import System.Info (os)+#endif #if __GLASGOW_HASKELL__ <= 610 import Prelude hiding (putStr, putStrLn) import System.IO.UTF8@@ -57,9 +60,12 @@ run cmd opts args where run cmd opts args- | Help `elem` opts = putStr usage+ | Help `elem` opts = putStr help1+ | HelpOptions `elem` opts = putStr help2+ | HelpAll `elem` opts = putStr $ help1 ++ "\n" ++ help2 | Version `elem` opts = putStrLn versionmsg | BinaryFilename `elem` opts = putStrLn binaryfilename+ | null cmd = maybe (putStr help1) (withJournalDo opts args cmd) defaultcmd | cmd `isPrefixOf` "balance" = withJournalDo opts args cmd balance | cmd `isPrefixOf` "convert" = withJournalDo opts args cmd convert | cmd `isPrefixOf` "print" = withJournalDo opts args cmd print'@@ -70,11 +76,19 @@ #ifdef VTY | cmd `isPrefixOf` "vty" = withJournalDo opts args cmd vty #endif-#if defined(WEB) || defined(WEBHAPPSTACK)+#if defined(WEB) || defined(WEB610) | cmd `isPrefixOf` "web" = withJournalDo opts args cmd web #endif #ifdef CHART | cmd `isPrefixOf` "chart" = withJournalDo opts args cmd chart #endif | cmd `isPrefixOf` "test" = runtests opts args >> return ()- | otherwise = putStr usage+ | otherwise = putStr help1++-- in a web-enabled build on windows, run the web ui by default+#if defined(WEB) || defined(WEB610)+ defaultcmd | os=="mingw32" = Just web+ | otherwise = Nothing+#else+ defaultcmd = Nothing+#endif
Hledger/Cli/Options.hs view
@@ -8,7 +8,7 @@ import System.Console.GetOpt import System.Environment import Hledger.Cli.Version (timeprogname)-import Hledger.Data.IO (myLedgerPath,myTimelogPath)+import Hledger.Read (myJournalPath, myTimelogPath) import Hledger.Data.Utils import Hledger.Data.Types import Hledger.Data.Dates@@ -20,29 +20,38 @@ chartsize = "600x400" #endif -usagehdr =- "Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]\n" +++help1 =+ "Usage: hledger [OPTIONS] COMMAND [PATTERNS]\n" ++ " hledger [OPTIONS] convert CSVFILE\n" ++ " hledger [OPTIONS] stats\n" ++ "\n" ++- "hledger uses your ~/.ledger or $LEDGER file, or another specified with -f\n" +++ "hledger reads your ~/.journal file, or another specified with $LEDGER or -f\n" ++ "\n" ++ "COMMAND is one of (may be abbreviated):\n" ++- " add - prompt for new transactions and add them to the ledger\n" +++ " add - prompt for new transactions and add them to the journal\n" ++ " balance - show accounts, with balances\n" ++- " convert - read CSV bank data and display in ledger format\n" +++ " convert - read CSV bank data and display in journal format\n" ++ " histogram - show a barchart of transactions per day or other interval\n" ++- " print - show transactions in ledger format\n" +++ " print - show transactions in journal format\n" ++ " register - show transactions as a register with running balance\n" ++- " stats - show various statistics for a ledger\n" +++ " stats - show various statistics for a journal\n" +++ " vty - run a simple curses-style UI" ++ #ifdef VTY- " vty - run a simple curses-style UI\n" +++ "\n" +++#else+ " (DISABLED, install with -fvty)\n" ++ #endif-#ifdef WEB- " web - run a simple web-based UI\n" +++ " web - run a simple web-based UI" +++#if defined(WEB) || defined(WEB610)+ "\n" +++#else+ " (DISABLED, install with -fweb or -fweb610)\n" ++ #endif+ " chart - generate balances pie charts" ++ #ifdef CHART- " chart - generate balances pie chart\n" +++ "\n" +++#else+ " (DISABLED, install with -fchart)\n" ++ #endif " test - run self-tests\n" ++ "\n" ++@@ -52,46 +61,55 @@ "\n" ++ "DATES can be y/m/d or ledger-style smart dates like \"last month\".\n" ++ "\n" ++- "Options:"+ "Use --help-options to see OPTIONS, or --help-all/-H.\n" +++ "" -usageftr = ""-usage = usageInfo usagehdr options ++ usageftr+help2 = usageInfo "Options:\n" options -- | Command-line options we accept. options :: [OptDescr Opt] options = [- Option "f" ["file"] (ReqArg File "FILE") "use a different ledger/timelog file; - means stdin"- ,Option "" ["no-new-accounts"] (NoArg NoNewAccts) "don't allow to create new accounts"+ Option "f" ["file"] (ReqArg File "FILE") "use a different journal/timelog file; - means stdin"+ ,Option "" ["no-new-accounts"] (NoArg NoNewAccts) "don't allow to create new accounts" ,Option "b" ["begin"] (ReqArg Begin "DATE") "report on transactions on or after this date" ,Option "e" ["end"] (ReqArg End "DATE") "report on transactions before this date" ,Option "p" ["period"] (ReqArg Period "EXPR") ("report on transactions during the specified period\n" ++- "and/or with the specified reporting interval\n")+ "and/or with the specified reporting interval\n") ,Option "C" ["cleared"] (NoArg Cleared) "report only on cleared transactions" ,Option "U" ["uncleared"] (NoArg UnCleared) "report only on uncleared transactions" ,Option "B" ["cost","basis"] (NoArg CostBasis) "report cost of commodities"- ,Option "" ["depth"] (ReqArg Depth "N") "hide accounts/transactions deeper than this"+ ,Option "" ["depth"] (ReqArg Depth "N") "hide accounts/transactions deeper than this" ,Option "d" ["display"] (ReqArg Display "EXPR") ("show only transactions matching EXPR (where\n" ++- "EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)")- ,Option "" ["effective"] (NoArg Effective) "use transactions' effective dates, if any"+ "EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)")+ ,Option "" ["effective"] (NoArg Effective) "use transactions' effective dates, if any" ,Option "E" ["empty"] (NoArg Empty) "show empty/zero things which are normally elided" ,Option "R" ["real"] (NoArg Real) "report only on real (non-virtual) transactions"- ,Option "" ["no-total"] (NoArg NoTotal) "balance report: hide the final total"--- ,Option "s" ["subtotal"] (NoArg SubTotal) "balance report: show subaccounts"- ,Option "W" ["weekly"] (NoArg WeeklyOpt) "register report: show weekly summary"- ,Option "M" ["monthly"] (NoArg MonthlyOpt) "register report: show monthly summary"- ,Option "Q" ["quarterly"] (NoArg QuarterlyOpt) "register report: show quarterly summary"- ,Option "Y" ["yearly"] (NoArg YearlyOpt) "register report: show yearly summary"- ,Option "h" ["help"] (NoArg Help) "show this help"- ,Option "V" ["version"] (NoArg Version) "show version information"- ,Option "v" ["verbose"] (NoArg Verbose) "show verbose test output"- ,Option "" ["binary-filename"] (NoArg BinaryFilename) "show the download filename for this hledger build"- ,Option "" ["debug"] (NoArg Debug) "show extra debug output; implies verbose"- ,Option "" ["debug-vty"] (NoArg DebugVty) "run vty command with no vty output, showing console"+ ,Option "" ["flat"] (NoArg Flat) "balance: show full account names, unindented"+ ,Option "" ["drop"] (ReqArg Drop "N") "balance: with --flat, elide first N account name components"+ ,Option "" ["no-total"] (NoArg NoTotal) "balance: hide the final total"+ ,Option "W" ["weekly"] (NoArg WeeklyOpt) "register, stats: report by week"+ ,Option "M" ["monthly"] (NoArg MonthlyOpt) "register, stats: report by month"+ ,Option "Q" ["quarterly"] (NoArg QuarterlyOpt) "register, stats: report by quarter"+ ,Option "Y" ["yearly"] (NoArg YearlyOpt) "register, stats: report by year" #ifdef CHART ,Option "o" ["output"] (ReqArg ChartOutput "FILE") ("chart: output filename (default: "++chartoutput++")") ,Option "" ["items"] (ReqArg ChartItems "N") ("chart: number of accounts to show (default: "++show chartitems++")") ,Option "" ["size"] (ReqArg ChartSize "WIDTHxHEIGHT") ("chart: image size (default: "++chartsize++")") #endif+#ifdef VTY+ ,Option "" ["debug-vty"] (NoArg DebugVty) "vty: run with no terminal output, showing console"+#endif+#ifdef WEB+ ,Option "" ["base-url"] (ReqArg BaseUrl "URL") "web: use this base url (default http://localhost:PORT)"+ ,Option "" ["port"] (ReqArg Port "N") "web: serve on tcp port N (default 5000)"+#endif+ ,Option "v" ["verbose"] (NoArg Verbose) "show more verbose output"+ ,Option "" ["debug"] (NoArg Debug) "show extra debug output; implies verbose"+ ,Option "" ["binary-filename"] (NoArg BinaryFilename) "show the download filename for this hledger build"+ ,Option "V" ["version"] (NoArg Version) "show version information"+ ,Option "h" ["help"] (NoArg Help) "show basic command-line usage"+ ,Option "" ["help-options"] (NoArg HelpOptions) "show command-line options"+ ,Option "H" ["help-all"] (NoArg HelpAll) "show command-line usage and options" ] -- | An option value from a command-line flag.@@ -109,13 +127,19 @@ Effective | Empty | Real | + Flat |+ Drop {value::String} | NoTotal | SubTotal | WeeklyOpt | MonthlyOpt | QuarterlyOpt | YearlyOpt |+ BaseUrl {value::String} |+ Port {value::String} | Help |+ HelpOptions |+ HelpAll | Verbose | Version | BinaryFilename@@ -148,14 +172,12 @@ parseArguments = do args <- liftM (map decodeString) getArgs let (os,as,es) = getOpt Permute options args--- istimequery <- usingTimeProgramName--- let os' = if istimequery then (Period "today"):os else os os' <- fixOptDates os let os'' = if Debug `elem` os' then Verbose:os' else os' case (as,es) of (cmd:args,[]) -> return (os'',cmd,args) ([],[]) -> return (os'',"",[])- (_,errs) -> ioError (userError (concat errs ++ usage))+ (_,errs) -> ioError (userError (concat errs ++ help1)) -- | Convert any fuzzy dates within these option values to explicit ones, -- based on today's date.@@ -202,13 +224,20 @@ periodopts = reverse $ optValuesForConstructor Period opts intervalopts = reverse $ filter (`elem` [WeeklyOpt,MonthlyOpt,QuarterlyOpt,YearlyOpt]) opts --- | Get the value of the (last) depth option, if any, otherwise a large number.+-- | Get the value of the (last) depth option, if any. depthFromOpts :: [Opt] -> Maybe Int depthFromOpts opts = listtomaybeint $ optValuesForConstructor Depth opts where listtomaybeint [] = Nothing listtomaybeint vs = Just $ read $ last vs +-- | Get the value of the (last) drop option, if any, otherwise 0.+dropFromOpts :: [Opt] -> Int+dropFromOpts opts = fromMaybe 0 $ listtomaybeint $ optValuesForConstructor Drop opts+ where+ listtomaybeint [] = Nothing+ listtomaybeint vs = Just $ read $ last vs+ -- | Get the value of the (last) display option, if any. displayExprFromOpts :: [Opt] -> Maybe String displayExprFromOpts opts = listtomaybe $ optValuesForConstructor Display opts@@ -216,13 +245,28 @@ listtomaybe [] = Nothing listtomaybe vs = Just $ last vs +-- | Get the value of the (last) baseurl option, if any.+baseUrlFromOpts :: [Opt] -> Maybe String+baseUrlFromOpts opts = listtomaybe $ optValuesForConstructor BaseUrl opts+ where+ listtomaybe [] = Nothing+ listtomaybe vs = Just $ last vs++-- | Get the value of the (last) port option, if any.+portFromOpts :: [Opt] -> Maybe Int+portFromOpts opts = listtomaybeint $ optValuesForConstructor Port opts+ where+ listtomaybeint [] = Nothing+ listtomaybeint vs = Just $ read $ last vs++ -- | Get a maybe boolean representing the last cleared/uncleared option if any. clearedValueFromOpts opts | null os = Nothing | last os == Cleared = Just True | otherwise = Just False where os = optsWithConstructors [Cleared,UnCleared] opts --- | Was the program invoked via the \"hours\" alias ?+-- | Were we invoked as \"hours\" ? usingTimeProgramName :: IO Bool usingTimeProgramName = do progname <- getProgName@@ -232,7 +276,7 @@ journalFilePathFromOpts :: [Opt] -> IO String journalFilePathFromOpts opts = do istimequery <- usingTimeProgramName- f <- if istimequery then myTimelogPath else myLedgerPath+ f <- if istimequery then myTimelogPath else myJournalPath return $ last $ f : optValuesForConstructor File opts -- | Gather filter pattern arguments into a list of account patterns and a
Hledger/Cli/Tests.hs view
@@ -15,7 +15,7 @@ source tree. They are no longer used, but here is an example: @-$ hledger -f sample.ledger balance o+$ hledger -f sample.journal balance o $1 expenses:food $-2 income $-1 gifts@@ -29,26 +29,26 @@ module Hledger.Cli.Tests where import qualified Data.Map as Map-import Test.HUnit.Tools (runVerboseTests) import System.Exit (exitFailure, exitWith, ExitCode(ExitSuccess)) -- base 3 compatible import System.Time (ClockTime(TOD)) import Hledger.Cli.Commands.All import Hledger.Data -- including testing utils in Hledger.Data.Utils+import Hledger.Read.Common (emptyCtx)+import Hledger.Read (readJournal)+import Hledger.Read.Journal (someamount) import Hledger.Cli.Options import Hledger.Cli.Utils -- | Run unit tests. runtests :: [Opt] -> [String] -> IO ()-runtests opts args = do- (counts,_) <- runner ts+runtests _ args = do+ (counts,_) <- liftM (flip (,) 0) $ runTestTT ts if errors counts > 0 || (failures counts > 0) then exitFailure else exitWith ExitSuccess where- runner | Verbose `elem` opts = runVerboseTests- | otherwise = liftM (flip (,) 0) . runTestTT ts = TestList $ filter matchname $ tflatten tests -- show flat test names -- ts = tfilter matchname $ TestList tests -- show hierarchical test names matchname = matchpats args . tname@@ -61,8 +61,8 @@ tests_Hledger_Commands, "account directive" ~:- let sameParse str1 str2 = do j1 <- journalFromString str1- j2 <- journalFromString str2+ let sameParse str1 str2 = do j1 <- readJournal Nothing str1 >>= either error return+ j2 <- readJournal Nothing str2 >>= either error return j1 `is` j2{filereadtime=filereadtime j1, jtext=jtext j1} in TestList [@@ -107,7 +107,7 @@ ,"balance report tests" ~: let (opts,args) `gives` es = do - l <- sampleledgerwithopts opts args+ l <- samplejournalwithopts opts args t <- getCurrentLocalTime showBalanceReport opts (optsToFilterSpec opts args t) l `is` unlines es in TestList@@ -125,6 +125,8 @@ ," $-1 gifts" ," $-1 salary" ," $1 liabilities:debts"+ ,"--------------------"+ ," $0" ] ,"balance report can be limited with --depth" ~:@@ -133,6 +135,8 @@ ," $2 expenses" ," $-2 income" ," $1 liabilities"+ ,"--------------------"+ ," $0" ] ,"balance report with account pattern o" ~:@@ -176,6 +180,8 @@ ," $-1 gifts" ," $-1 salary" ," $1 liabilities:debts"+ ,"--------------------"+ ," $0" ] ,"balance report with unmatched parent of two matched subaccounts" ~: @@ -208,7 +214,10 @@ ] ,"balance report negative account pattern always matches full name" ~: - ([], ["not:e"]) `gives` []+ ([], ["not:e"]) `gives`+ ["--------------------"+ ," 0"+ ] ,"balance report negative patterns affect totals" ~: ([], ["expenses","not:food"]) `gives`@@ -229,22 +238,23 @@ ] ,"balance report with cost basis" ~: do- j <- journalFromString $ unlines+ j <- (readJournal Nothing $ unlines ["" ,"2008/1/1 test " ," a:b 10h @ $50" ," c:d "- ,""- ]+ ]) >>= either error return let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment showBalanceReport [] nullfilterspec j' `is` unlines [" $500 a:b" ," $-500 c:d"+ ,"--------------------"+ ," $0" ] ,"balance report elides zero-balance root account(s)" ~: do- l <- journalFromStringWithOpts []+ l <- readJournalWithOpts [] (unlines ["2008/1/1 one" ," test:a 1"@@ -254,6 +264,8 @@ unlines [" 1 test:a" ," -1 test:b"+ ,"--------------------"+ ," 0" ] ]@@ -372,15 +384,10 @@ "assets:bank" `isSubAccountNameOf` "my assets" `is` False ,"default year" ~: do- rl <- journalFromString defaultyear_ledger_str+ rl <- readJournal Nothing defaultyear_journal_str >>= either error return tdate (head $ jtxns rl) `is` fromGregorian 2009 1 1 return () - ,"ledgerFile" ~: do- assertBool "ledgerFile should parse an empty file" (isRight $ parseWithCtx emptyCtx ledgerFile "")- r <- journalFromString "" -- don't know how to get it from ledgerFile- assertBool "ledgerFile parsing an empty file should give an empty ledger" $ null $ jtxns r- ,"normaliseMixedAmount" ~: do normaliseMixedAmount (Mixed []) ~?= Mixed [nullamt] @@ -403,7 +410,7 @@ "print expenses" ~: do let args = ["expenses"]- l <- sampleledgerwithopts [] args+ l <- samplejournalwithopts [] args t <- getCurrentLocalTime showTransactions (optsToFilterSpec [] args t) l `is` unlines ["2008/06/03 * eat & shop"@@ -415,7 +422,7 @@ , "print report with depth arg" ~: do - l <- sampleledger+ l <- samplejournal t <- getCurrentLocalTime showTransactions (optsToFilterSpec [Depth "2"] [] t) l `is` unlines ["2008/01/01 income"@@ -450,7 +457,7 @@ "register report with no args" ~: do - l <- sampleledger+ l <- samplejournal showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines ["2008/01/01 income assets:bank:checking $1 $1" ," income:salary $-1 0"@@ -468,7 +475,7 @@ ,"register report with cleared option" ~: do let opts = [Cleared]- l <- journalFromStringWithOpts opts sample_ledger_str+ l <- readJournalWithOpts opts sample_journal_str showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines ["2008/06/03 eat & shop expenses:food $1 $1" ," expenses:supplies $1 $2"@@ -480,7 +487,7 @@ ,"register report with uncleared option" ~: do let opts = [UnCleared]- l <- journalFromStringWithOpts opts sample_ledger_str+ l <- readJournalWithOpts opts sample_journal_str showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines ["2008/01/01 income assets:bank:checking $1 $1" ," income:salary $-1 0"@@ -492,7 +499,7 @@ ,"register report sorts by date" ~: do - l <- journalFromStringWithOpts [] $ unlines+ l <- readJournalWithOpts [] $ unlines ["2008/02/02 a" ," b 1" ," c"@@ -505,21 +512,21 @@ ,"register report with account pattern" ~: do- l <- sampleledger+ l <- samplejournal showRegisterReport [] (optsToFilterSpec [] ["cash"] t1) l `is` unlines ["2008/06/03 eat & shop assets:cash $-2 $-2" ] ,"register report with account pattern, case insensitive" ~: do - l <- sampleledger+ l <- samplejournal showRegisterReport [] (optsToFilterSpec [] ["cAsH"] t1) l `is` unlines ["2008/06/03 eat & shop assets:cash $-2 $-2" ] ,"register report with display expression" ~: do - l <- sampleledger+ l <- samplejournal let gives displayexpr = (registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l) `is`) where opts = [Display displayexpr]@@ -531,9 +538,9 @@ ,"register report with period expression" ~: do - l <- sampleledger + l <- samplejournal let periodexpr `gives` dates = do- l' <- sampleledgerwithopts opts []+ l' <- samplejournalwithopts opts [] registerdates (showRegisterReport opts (optsToFilterSpec opts [] t1) l') `is` dates where opts = [Period periodexpr] "" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]@@ -561,7 +568,7 @@ , "register report with depth arg" ~: do - l <- sampleledger+ l <- samplejournal let opts = [Depth "2"] showRegisterReport opts (optsToFilterSpec opts [] t1) l `is` unlines ["2008/01/01 income income:salary $-1 $-1"@@ -577,14 +584,17 @@ ,"show hours" ~: show (hours 1) ~?= "1.0h" ,"unicode in balance layout" ~: do- l <- journalFromStringWithOpts []+ l <- readJournalWithOpts [] "2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n" showBalanceReport [] (optsToFilterSpec [] [] t1) l `is` unlines [" -100 актив:наличные"- ," 100 расходы:покупки"]+ ," 100 расходы:покупки"+ ,"--------------------"+ ," 0"+ ] ,"unicode in register layout" ~: do- l <- journalFromStringWithOpts []+ l <- readJournalWithOpts [] "2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n" showRegisterReport [] (optsToFilterSpec [] [] t1) l `is` unlines ["2009/01/01 медвежья шкура расходы:покупки 100 100"@@ -626,7 +636,7 @@ -- "next january" `gives` "2009/01/01" ,"subAccounts" ~: do- l <- liftM (journalToLedger nullfilterspec) sampleledger+ l <- liftM (journalToLedger nullfilterspec) samplejournal let a = ledgerAccount l "assets" map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"] @@ -673,11 +683,11 @@ date1 = parsedate "2008/11/26" t1 = LocalTime date1 midday -sampleledger = journalFromStringWithOpts [] sample_ledger_str-sampleledgerwithopts opts _ = journalFromStringWithOpts opts sample_ledger_str+samplejournal = readJournalWithOpts [] sample_journal_str+samplejournalwithopts opts _ = readJournalWithOpts opts sample_journal_str -sample_ledger_str = unlines- ["; A sample ledger file."+sample_journal_str = unlines+ ["; A sample journal file." ,";" ,"; Sets up this account tree:" ,"; assets"@@ -719,7 +729,7 @@ ,";final comment" ] -defaultyear_ledger_str = unlines+defaultyear_journal_str = unlines ["Y2009" ,"" ,"01/01 A"@@ -727,7 +737,7 @@ ," b" ] -write_sample_ledger = writeFile "sample.ledger" sample_ledger_str+write_sample_journal = writeFile "sample.journal" sample_journal_str entry2_str = unlines ["2007/01/27 * joes diner"@@ -777,7 +787,7 @@ ,"" ] -ledger1_str = unlines+journal1_str = unlines ["" ,"2007/01/27 * joes diner" ," expenses:food:dining $10.00"@@ -792,7 +802,7 @@ ,"" ] -ledger2_str = unlines+journal2_str = unlines [";comment" ,"2007/01/27 * joes diner" ," expenses:food:dining $10.00"@@ -800,7 +810,7 @@ ,"" ] -ledger3_str = unlines+journal3_str = unlines ["2007/01/27 * joes diner" ," expenses:food:dining $10.00" ,";intra-entry comment"@@ -808,7 +818,7 @@ ,"" ] -ledger4_str = unlines+journal4_str = unlines ["!include \"somefile\"" ,"2007/01/27 * joes diner" ," expenses:food:dining $10.00"@@ -816,9 +826,9 @@ ,"" ] -ledger5_str = ""+journal5_str = "" -ledger6_str = unlines+journal6_str = unlines ["~ monthly from 2007/1/21" ," expenses:entertainment $16.23 ;netflix" ," assets:checking"@@ -829,7 +839,7 @@ ,"" ] -ledger7_str = unlines+journal7_str = unlines ["2007/01/01 * opening balance" ," assets:cash $4.82" ," equity:opening balances "@@ -1049,7 +1059,7 @@ ledger7 = journalToLedger nullfilterspec journal7 -ledger8_str = unlines+journal8_str = unlines ["2008/1/1 test " ," a:b 10h @ $40" ," c:d "@@ -1078,5 +1088,5 @@ "" (TOD 0 0) ""- where parse = fromparse . parseWithCtx emptyCtx postingamount . (" "++)+ where parse = fromparse . parseWithCtx emptyCtx someamount
Hledger/Cli/Utils.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE CPP #-} {-| -Utilities for top-level modules and ghci. See also Hledger.Data.IO and+Utilities for top-level modules and ghci. See also Hledger.Read and Hledger.Data.Utils. -}@@ -9,49 +9,80 @@ module Hledger.Cli.Utils ( withJournalDo,- journalFromStringWithOpts,- openBrowserOn+ readJournalWithOpts,+ journalReload,+ journalReloadIfChanged,+ journalFileIsNewer,+ journalFileModificationTime,+ openBrowserOn,+ writeFileWithBackup,+ writeFileWithBackupIfChanged, ) where-import Control.Monad.Error import Hledger.Data+import Hledger.Read import Hledger.Cli.Options (Opt(..),journalFilePathFromOpts) -- ,optsToFilterSpec)-import System.Directory (doesFileExist)-import System.IO (stderr)-#if __GLASGOW_HASKELL__ <= 610-import System.IO.UTF8 (hPutStrLn)-#else-import System.IO (hPutStrLn)-#endif+import Safe (readMay)+import System.Directory (getModificationTime, getDirectoryContents, copyFile) import System.Exit-import System.Process (readProcessWithExitCode)+import System.FilePath ((</>), splitFileName, takeDirectory) import System.Info (os)+import System.Process (readProcessWithExitCode)+import System.Time (ClockTime, getClockTime, diffClockTimes, TimeDiff(TimeDiff)) -- | Parse the user's specified journal file and run a hledger command on--- it, or report a parse error. This function makes the whole thing go.+-- it, or throw an error. withJournalDo :: [Opt] -> [String] -> String -> ([Opt] -> [String] -> Journal -> IO ()) -> IO ()-withJournalDo opts args cmdname cmd = do+withJournalDo opts args _ cmd = do -- We kludgily read the file before parsing to grab the full text, unless -- it's stdin, or it doesn't exist and we are adding. We read it strictly -- to let the add command work.- f <- journalFilePathFromOpts opts- fileexists <- doesFileExist f- let creating = not fileexists && cmdname == "add"+ journalFilePathFromOpts opts >>= readJournalFile Nothing >>= either error runcmd+ where costify = (if CostBasis `elem` opts then journalConvertAmountsToCost else id) runcmd = cmd opts args . costify- if creating- then runcmd nulljournal- else (runErrorT . parseJournalFile) f >>= either parseerror runcmd- where parseerror e = hPutStrLn stderr e >> exitWith (ExitFailure 1) -- | Get a journal from the given string and options, or throw an error.-journalFromStringWithOpts :: [Opt] -> String -> IO Journal-journalFromStringWithOpts opts s = do- j <- journalFromString s- let cost = CostBasis `elem` opts- return $ (if cost then journalConvertAmountsToCost else id) j+readJournalWithOpts :: [Opt] -> String -> IO Journal+readJournalWithOpts opts s = do+ j <- readJournal Nothing s >>= either error return+ return $ (if cost then journalConvertAmountsToCost else id) j+ where cost = CostBasis `elem` opts +-- | Re-read a journal from its data file, or return an error string.+journalReload :: Journal -> IO (Either String Journal)+journalReload Journal{filepath=f} = readJournalFile Nothing f++-- | Re-read a journal from its data file mostly, only if the file has+-- changed since last read (or if there is no file, ie data read from+-- stdin). The provided options are mostly ignored. Return a journal or+-- the error message while reading it, and a flag indicating whether it+-- was re-read or not.+journalReloadIfChanged :: [Opt] -> Journal -> IO (Either String Journal, Bool)+journalReloadIfChanged opts j@Journal{filepath=f} = do+ changed <- journalFileIsNewer j+ if changed+ then do+ when (Verbose `elem` opts) $ printf "%s has changed, reloading\n" f+ jE <- journalReload j+ return (jE, True)+ else+ return (Right j, False)++-- | Has the journal's data file changed since last parsed ?+journalFileIsNewer :: Journal -> IO Bool+journalFileIsNewer j@Journal{filereadtime=tread} = do+ tmod <- journalFileModificationTime j+ return $ diffClockTimes tmod tread > (TimeDiff 0 0 0 0 0 0 0)++-- | Get the last modified time of the journal's data file (or if there is no+-- file, the current time).+journalFileModificationTime :: Journal -> IO ClockTime+journalFileModificationTime Journal{filepath=f}+ | null f = getClockTime+ | otherwise = getModificationTime f `Prelude.catch` \_ -> getClockTime+ -- | Attempt to open a web browser on the given url, all platforms. openBrowserOn :: String -> IO ExitCode openBrowserOn u = trybrowsers browsers u@@ -62,16 +93,49 @@ ExitSuccess -> return ExitSuccess ExitFailure _ -> trybrowsers bs u trybrowsers [] u = do- putStrLn $ printf "Sorry, I could not start a browser (tried: %s)" $ intercalate ", " browsers+ putStrLn $ printf "Could not start a web browser (tried: %s)" $ intercalate ", " browsers putStrLn $ printf "Please open your browser and visit %s" u return $ ExitFailure 127 browsers | os=="darwin" = ["open"]- | os=="mingw32" = ["start"]+ | os=="mingw32" = ["c:/Program Files/Mozilla Firefox/firefox.exe"] | otherwise = ["sensible-browser","gnome-www-browser","firefox"] -- jeffz: write a ffi binding for it using the Win32 package as a basis -- start by adding System/Win32/Shell.hsc and follow the style of any -- other module in that directory for types, headers, error handling and -- what not. -- ::ShellExecute(NULL, "open", "www.somepage.com", NULL, NULL, SW_SHOWNORMAL);- -- ::ShellExecute(NULL, "open", "firefox.exe", "www.somepage.com" NULL, SW_SHOWNORMAL); +-- | Back up this file with a (incrementing) numbered suffix then+-- overwrite it with this new text, or give an error, but only if the text+-- is different from the current file contents, and return a flag+-- indicating whether we did anything.+writeFileWithBackupIfChanged :: FilePath -> String -> IO Bool+writeFileWithBackupIfChanged f t = do+ s <- readFile f+ if t == s then return False+ else backUpFile f >> writeFile f t >> return True++-- | Back up this file with a (incrementing) numbered suffix, then+-- overwrite it with this new text, or give an error.+writeFileWithBackup :: FilePath -> String -> IO ()+writeFileWithBackup f t = backUpFile f >> writeFile f t++-- | Back up this file with a (incrementing) numbered suffix, or give an error.+backUpFile :: FilePath -> IO ()+backUpFile fp = do+ fs <- safeGetDirectoryContents $ takeDirectory $ fp+ let (d,f) = splitFileName fp+ versions = catMaybes $ map (f `backupNumber`) fs+ next = maximum (0:versions) + 1+ f' = printf "%s.%d" f next+ copyFile fp (d </> f')++safeGetDirectoryContents :: FilePath -> IO [FilePath]+safeGetDirectoryContents "" = getDirectoryContents "."+safeGetDirectoryContents fp = getDirectoryContents fp++-- | Does the second file represent a backup of the first, and if so which version is it ?+backupNumber :: FilePath -> FilePath -> Maybe Int+backupNumber f g = case matchRegexPR ("^" ++ f ++ "\\.([0-9]+)$") g of+ Just (_, ((_,suffix):_)) -> readMay suffix+ _ -> Nothing
Hledger/Cli/Version.hs view
@@ -70,8 +70,8 @@ ,"vty" #endif #if defined(WEB)- ,"web (using simpleserver)"-#elif defined(WEBHAPPSTACK)- ,"web (using happstack)"+ ,"web (using yesod/hamlet/simpleserver)"+#elif defined(WEB610)+ ,"web (using loli/hsp/simpleserver)" #endif ]
MANUAL.markdown view
@@ -4,9 +4,9 @@ # hledger manual -This is the official hledger manual. You may also want to visit the-[http://hledger.org](http://hledger.org) home page, the-[hledger for techies](README2.html) page, and for background,+This is the official hledger manual, for version 0.11.0. You may also+want to visit the rest of [hledger.org](http://hledger.org), and for+background, [c++ ledger's manual](http://joyful.com/repos/ledger/doc/ledger.html). ## User Guide@@ -24,8 +24,8 @@ programming language rather than in C++. hledger's basic function is to generate register and balance reports from-a plain text ledger file, at the command line or via the web or curses-interface. You can use it to, eg,+a plain text general journal file, at the command line or via the web or+curses interface. You can use it to, eg, - track spending and income - see time reports by day/week/month/project@@ -55,80 +55,34 @@ be able to use platform packages; eg on Ubuntu Lucid, do `apt-get install ghc6 cabal-install happy`. -2. Make sure ~/.cabal/bin is in your path. This is useful so that you can- run hledger by just typing "hledger", and necessary if (eg) you install- with -fweb, to avoid an installation failure..--3. Install hledger with cabal-install:+2. Install hledger with cabal-install. Make sure ~/.cabal/bin is in your+ path; this is required while installing some cabal packages. Eg: + export PATH=$PATH:~/.cabal/bin cabal update cabal install hledger - You can add the following options to the install command to include extra features- (these are not enabled by default as they are harder to build):-- - `-fvty` - builds the [ui](#ui) command. (Not available on microsoft- windows.)-- - `-fweb` - builds the [web](#web) command.-- - `-fchart` builds the [chart](#chart) command. This requires- [gtk2hs](http://www.haskell.org/gtk2hs/download/), which you'll- need to install yourself as it's not yet provided by the haskell- platform or cabal.--#### Installation Problems--The above builds a lot of software, and it's not always smooth sailing.-Here are some known issues and things to try:--- **Ask for help on [#hledger](irc://freenode.net/#hledger) or [#haskell](irc://freenode.net/#haskell)**--- **cabal update.** If you didn't already, ``cabal update`` and try again.--- **Do you have a new enough version of GHC ?** As of 2010, 6.10 and 6.12- are supported, 6.8 might or might not work.--- **Do you have a new enough version of cabal-install ?**- Newer versions tend to be better at resolving problems. 0.6.2 has been- known to fail where newer versions succeed.--- **Installation fails, reporting problems with a hledger package.**- That hledger release might have a coding error (heavens), or- compatibility problems have been revealed as dependencies have been- updated. You could try installing the [previous hledger- version](http://hackage.haskell.org/package/hledger) (``cabal- install hledger-0.x``) or, preferably, the latest hledger- development code, which is likely to work best. In a nutshell,- install [darcs](http://darcs.net) and:-- darcs get --lazy http://joyful.com/repos/hledger- cd hledger/hledger-lib; cabal install- cd ..; cabal install [-fweb] [-fvty]--- **Installation fails, reporting problems with packages A, B and C.**- Resolve the problem packages one at a time. Eg, cabal install A. Look- for the cause of the failure near the end of the output. If it's not- apparent, try again with `-v2` or `-v3` for more verbose output.+ You can add the following options to the install command to build+ extra features (if you're new to cabal, I recommend you get the basic+ install working first, then add these one at a time): -- **Could not run trhsx.**- You are installing -fweb, which needs to run the ``trhsx`` executable.- It is usually installed in ~/.cabal/bin, which may not be in your path.+ - `-fchart` builds the [chart](#chart) command, enabling simple+ balance pie chart generation. This requires additional GTK/GHC+ integration libraries (on ubuntu: `apt-get install libghc6-gtk-dev`)+ and possibly other things - see the+ [gtk2hs install docs](http://code.haskell.org/gtk2hs/INSTALL).+ At present this add a lot of build complexity for not much gain. -- **Could not run happy.**- A package (eg haskell-src-exts) needs to run the ``happy`` executable.- If not using the haskell platform, install the appropriate platform- package which provides it (eg apt-get install happy).+ - `-fvty` builds the [vty](#vty) command, enabling a basic+ curses-style user interface. This does not work on microsoft+ windows, unless possibly with cygwin. -- **A ghc panic while building** might be due to- [http://hackage.haskell.org/trac/ghc/ticket/3862](http://hackage.haskell.org/trac/ghc/ticket/3862)+ - `-fweb` builds the [web](#web) command, enabling a web-based user+ interface. This requires GHC 6.12. If you are stuck with GHC 6.10,+ you can use `-fweb610` instead, to build an older version of the web+ interface. -- **cabal could not reconcile dependencies**- In some cases, especially if you have installed/updated many packages or- GHC itself, cabal may not be able to figure out the installation. This- can also arise due to non-optimal dependency information configured in- hledger or its dependencies. You can sometimes work around this by using- cabal's `--constraint` option.+If you have any trouble, proceed at once to [Troubleshooting](#troubleshooting) for help! ### Basic usage @@ -140,16 +94,16 @@ [COMMAND](#commands) is one of: add, balance, chart, convert, histogram, print, register, stats, ui, web, test (defaulting to balance). The optional [PATTERNS](#filter-patterns) are regular expressions which select-a subset of the ledger data.+a subset of the journal data. -hledger looks for data in a ledger file, usually `.ledger` in your home+hledger looks for data in a journal file, usually `.journal` in your home directory. You can specify a different file with the -f option (use - for standard input) or `LEDGER` environment variable. -To get started, make yourself a ledger file containing some+To get started, make yourself a journal file containing some transactions. You can copy the sample file below (or-[sample.ledger](http://joyful.com/repos/hledger/sample.ledger)) and save-it as `.ledger` in your home directory. Or, just run `hledger add` and+[sample.journal](http://joyful.com/repos/hledger/data/sample.journal)) and save+it as `.journal` in your home directory. Or, just run `hledger add` and enter a few transactions. Now you can try some of these commands, or read on: @@ -161,21 +115,24 @@ hledger reg checking # checking transactions hledger reg desc:shop # transactions with shop in the description hledger histogram # transactions per day, or other interval- hledger add # add some new transactions to the ledger file- hledger ui # curses ui, if installed with -fvty- hledger web # web ui, if installed with -fweb+ hledger add # add some new transactions to the journal file+ hledger vty # curses ui, if installed with -fvty+ hledger web # web ui, if installed with -fweb or -fweb610 hledger chart # make a balance chart, if installed with -fchart You'll find more examples below. -### File format+<a name="file-format" /> -hledger's data file, aka the ledger, is a plain text representation of a-standard accounting journal. It contains a number of transactions, each-describing a transfer of money (or another commodity) between two or more-named accounts. Here's an example:+### Journal file - ; A sample ledger file. This is a comment.+hledger's data file, aka the journal, is a standard accounting+[general journal](http://en.wikipedia.org/wiki/General_journal) in a plain+text format. It contains a number of transactions, each describing a+transfer of money (or another commodity) between two or more named+accounts. Here's an example:++ ; A sample journal file. This is a comment. 2008/01/01 income ; <- transaction's first line starts in column 0, contains date and description assets:bank:checking $1 ; <- posting lines start with whitespace, each contains an account name@@ -198,19 +155,21 @@ liabilities:debts $1 assets:bank:checking -Each transaction has a date, description, and two or more postings (of-some amount to some account) which must balance to 0. As a convenience,-one posting's amount may be left blank and will be inferred.+Each transaction has a date, optional description, and two or more+postings (of some amount to some account) which must balance to 0. As a+convenience, one posting's amount may be left blank and will be inferred. Note that account names may contain single spaces, while the amount must be separated from the account name by at least two spaces. -An amount is a number, with an optional currency/commodity symbol or word-on either the left or right. Note: when writing a negative amount with a-left-side currency symbol, the minus goes after the symbol, eg `$-1`.+An amount is a number, with an optional currency symbol or commodity name+on either the left or right. Commodity names which contain more than just+letters should be enclosed in double quotes. Negative amounts usually have+the minus sign next to the number (`$-1`), but it may also go before the+currency symbol/commodity name (`-$1`). -This file format is also compatible with c++ ledger, so you can use both-tools. For more details, see+hledger's file format aims to be compatible with c++ ledger, so you can+use both tools. For more details, see [File format compatibility](#file-format-compatibility). ## Reference@@ -218,7 +177,7 @@ ### Overview This version of hledger mimics a subset of ledger 3.x, and adds some-features of its own. We currently support regular ledger entries, timelog+features of its own. We currently support regular journal transactions, timelog entries, multiple commodities, (fixed) price history, virtual postings, filtering by account and description, the familiar print, register & balance commands and several new commands. We handle (almost) the full@@ -227,62 +186,65 @@ Here is the command-line help: - Usage: hledger [OPTIONS] [COMMAND [PATTERNS]]+ Usage: hledger [OPTIONS] COMMAND [PATTERNS] hledger [OPTIONS] convert CSVFILE hledger [OPTIONS] stats- - hledger uses your ~/.ledger or $LEDGER file, or another specified with -f- ++ hledger reads your ~/.journal file, or another specified with $LEDGER or -f FILE+ COMMAND is one of (may be abbreviated):- add - prompt for new transactions and add them to the ledger- balance - show accounts, with balances- convert - read CSV bank data and display in ledger format- histogram - show a barchart of transactions per day or other interval- print - show transactions in ledger format- register - show transactions as a register with running balance- stats - show various statistics for a ledger- ui - run a simple text-based UI- web - run a simple web-based UI- chart - generate balances pie chart- test - run self-tests- + add - prompt for new transactions and add them to the journal+ balance - show accounts, with balances+ convert - read CSV bank data and display in journal format+ histogram - show a barchart of transactions per day or other interval+ print - show transactions in journal format+ register - show transactions as a register with running balance+ stats - show various statistics for a journal+ vty - run a simple curses-style UI (if installed with -fvty)+ web - run a simple web-based UI (if installed with -fweb or -fweb610)+ chart - generate balances pie charts (if installed with -fchart)+ test - run self-tests+ PATTERNS are regular expressions which filter by account name. Prefix with desc: to filter by transaction description instead. Prefix with not: to negate a pattern. When using both, not: comes last.- + DATES can be y/m/d or ledger-style smart dates like "last month".- ++ Use --help-options to see OPTIONS, or --help-all/-H.+ Options:- -f FILE --file=FILE use a different ledger/timelog file; - means stdin- --no-new-accounts don't allow to create new accounts- -b DATE --begin=DATE report on transactions on or after this date- -e DATE --end=DATE report on transactions before this date- -p EXPR --period=EXPR report on transactions during the specified period- and/or with the specified reporting interval- -C --cleared report only on cleared transactions- -U --uncleared report only on uncleared transactions- -B --cost, --basis report cost of commodities- --depth=N hide accounts/transactions deeper than this- -d EXPR --display=EXPR show only transactions matching EXPR (where- EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)- --effective use transactions' effective dates, if any- -E --empty show empty/zero things which are normally elided- -R --real report only on real (non-virtual) transactions- --no-total balance report: hide the final total- -W --weekly register report: show weekly summary- -M --monthly register report: show monthly summary- -Q --quarterly register report: show quarterly summary- -Y --yearly register report: show yearly summary- -h --help show this help- -V --version show version information- -v --verbose show verbose test output- --binary-filename show the download filename for this hledger build- --debug show extra debug output; implies verbose- --debug-no-ui run ui commands with no output- -o FILE --output=FILE chart: output filename (default: hledger.png)- --items=N chart: number of accounts to show (default: 10)- --size=WIDTHxHEIGHT chart: image size (default: 600x400) + -f FILE --file=FILE use a different journal/timelog file; - means stdin+ --no-new-accounts don't allow to create new accounts+ -b DATE --begin=DATE report on transactions on or after this date+ -e DATE --end=DATE report on transactions before this date+ -p EXPR --period=EXPR report on transactions during the specified period+ and/or with the specified reporting interval+ -C --cleared report only on cleared transactions+ -U --uncleared report only on uncleared transactions+ -B --cost, --basis report cost of commodities+ --depth=N hide accounts/transactions deeper than this+ -d EXPR --display=EXPR show only transactions matching EXPR (where+ EXPR is 'dOP[DATE]' and OP is <, <=, =, >=, >)+ --effective use transactions' effective dates, if any+ -E --empty show empty/zero things which are normally elided+ -R --real report only on real (non-virtual) transactions+ --flat balance: show full account names, unindented+ --drop=N balance: with --flat, elide first N account name components+ --no-total balance: hide the final total+ -W --weekly register, stats: report by week+ -M --monthly register, stats: report by month+ -Q --quarterly register, stats: report by quarter+ -Y --yearly register, stats: report by year+ -v --verbose show more verbose output+ --debug show extra debug output; implies verbose+ --binary-filename show the download filename for this hledger build+ -V --version show version information+ -h --help show basic command-line usage+ --help-options show command-line options+ -H --help-all show command-line usage and options+ ### Commands #### Reporting commands@@ -291,9 +253,9 @@ ##### print -The print command displays full transactions from the ledger file, tidily+The print command displays full transactions from the journal file, tidily formatted and showing all amounts explicitly. The output of print is-always valid ledger data.+always a valid hledger journal. hledger's print command also shows all unit prices in effect, or (with -B/--cost) shows cost amounts.@@ -314,20 +276,32 @@ $ hledger register $ hledger register --monthly -E rent -##### balance+Note `--depth` doesn't work too well with `register` currently;+it hides deeper postings rather than aggregating them. -The balance command displays accounts and their balances.+##### balance +The balance command displays accounts and their balances, indented to show the account hierarchy. Examples: $ hledger balance $ hledger balance food -p 'last month'- $ for y in 2006 2007 2008 2009 2010; do echo; echo $y; hledger -f $y.ledger balance ^expenses --depth 2; done -##### chart+A final total is displayed, use `--no-total` to suppress this. Also, the+`--depth N` option shows accounts only to the specified depth, useful for+an overview: -(optional feature)+ $ for y in 2006 2007 2008 2009 2010; do echo; echo $y; hledger -f $y.journal balance ^expenses --depth 2; done +With `--flat`, a non-hierarchical list of full account names is displayed+instead. This mode shows just the accounts actually contributing to the+balance, making the arithmetic a little more obvious to non-hledger users.+In this mode you can also use `--drop N` to elide the first few account+name components. Note `--depth` doesn't work too well with `--flat` currently;+it hides deeper accounts rather than aggregating them.++##### chart+ The chart command saves a pie chart of your top account balances to an image file (usually "hledger.png", or use -o/--output FILE). You can adjust the image resolution with --size=WIDTHxHEIGHT, and the number of@@ -349,6 +323,8 @@ $ hledger chart ^expenses -o balance.png --size 1000x600 --items 20 $ for m in 01 02 03 04 05 06 07 08 09 10 11 12; do hledger -p 2009/$m chart ^expenses --depth 2 -o expenses-2009$m.png --size 400x300; done +This is an optional feature; see [installing](#installing).+ ##### histogram The histogram command displays a quick bar chart showing transaction@@ -361,74 +337,101 @@ ##### stats -The stats command displays quick summary information for the ledger.+The stats command displays quick summary information for the whole journal,+or by period. Examples: $ hledger stats--##### ui+ $ hledger stats -p 'monthly in 2009' -(optional feature)+##### vty -The ui command starts hledger's curses (full-screen, text) user interface,+The vty command starts hledger's curses (full-screen, text) user interface, which allows interactive navigation of the print/register/balance reports. This lets you browse around your numbers and get quick insights with less typing. Examples: -$ hledger ui $ hledger ui -BE food+ $ hledger vty+ $ hledger vty -BE food +This is an optional feature; see [installing](#installing).+ #### Modifying commands -The following commands can alter your ledger file.+The following commands can alter your journal file. ##### add The add command prompts interactively for new transactions, and adds them-to the ledger. It is experimental.+to the journal. It is experimental. Examples: -$ hledger add $ hledger add accounts:personal:bob+ $ hledger add+ $ hledger add accounts:personal:bob ##### web -(optional feature)- The web command starts hledger's web interface, and tries to open a web-browser to view it (if this fails, you'll have to visit the indicated url-yourself.) The web ui combines the features of the print, register,-balance and add commands.+browser to view it. (If this fails, you'll have to manually visit the url+it displays.) The web interface combines the features of the print,+register, balance and add commands, and adds a general edit command. +This is an optional feature. Note there is also an older implementation of+the web command which does not provide edit. See [installing](#installing).+ Examples: $ hledger web- $ hledger web --debug -f demo.ledger -p thisyear+ $ hledger web -E -B --depth 2+ $ hledger web --port 5010 --base-url http://some.vhost.com --debug -f my.journal +Warning: unlike all other hledger features, the edit form can alter your+existing journal data. You can edit, or erase, the journal file through+the web ui. There is currently no access control. A numbered backup of the+file will be saved at each edit, in normal circumstances (eg if file+permissions allow, disk is not full, etc.)++There are some options specific to the web server:++ --port=N web: serve on tcp port N (default 5000)++hledger will serve pages on port 5000 by default.++ --base-url=URL web: use this base url (default http://localhost:PORT)++Hyperlinks in the web interface all point to "localhost" by default, so if+you want to visit the hledger web server from other machines, you'll need+to use this option. Just give your machine's host name or ip address+instead of localhost. This option may also be useful when running hledger+behind a reverse proxy, to conform to your url scheme. Note that the PORT+in the base url need not be the same as the `--port` argument.+ #### Other commands ##### convert The convert command reads a [CSV](http://en.wikipedia.org/wiki/Comma-separated_values) file you have-downloaded from your bank, and prints out the transactions in ledger-format, suitable for adding to your ledger. It does not alter your ledger+downloaded from your bank, and prints out the transactions in journal+format, suitable for adding to your journal. It does not alter your journal directly. This can be a lot quicker than entering every transaction by hand. (The downside is that you are less likely to notice if your bank makes an error!) Use it like this: - $ hledger convert FILE.csv >FILE.ledger+ $ hledger convert FILE.csv >FILE.journal where FILE.csv is your downloaded csv file. This will convert the csv data using conversion rules defined in FILE.rules (auto-creating this file if-needed), and save the output into a temporary ledger file. Then you should-review FILE.ledger for problems; update the rules and convert again if+needed), and save the output into a temporary journal file. Then you should+review FILE.journal for problems; update the rules and convert again if needed; and finally copy/paste transactions which are new into your main-ledger.+journal. ###### .rules file @@ -444,7 +447,7 @@ amount-field 1 currency $ - # account-assigning rules+ ; account-assigning rules: SPECTRUM expenses:health:gym@@ -458,7 +461,7 @@ This says: -- the ledger account corresponding to this csv file is+- the account corresponding to this csv file is assets:bank:checking - the first csv field is the date, the second is the amount, the fifth is the description@@ -472,8 +475,8 @@ Notes: -- Lines beginning with \# or ; are ignored (but avoid using- inside an account rule)+- Lines beginning with ; or \# are ignored (but avoid using inside an+ account rule) - Definitions must come first, one per line, all in one paragraph. Each is a name and a value separated by whitespace.@@ -543,7 +546,7 @@ ##### Simple dates -Within a ledger file, dates must follow a fairly simple year/month/day+Within a journal file, dates must follow a fairly simple year/month/day format. Examples: > `2010/01/31` or `2010/1/31` or `2010-1-31` or `2010.1.31`@@ -553,7 +556,7 @@ ##### Default year -You can set a default year with a `Y` directive in the ledger, then+You can set a default year with a `Y` directive in the journal, then subsequent dates may be written as month/day. Eg: Y2009@@ -564,34 +567,33 @@ 1/31 ... -##### Actual and effective dates--Frequently, a real-life transaction has two (or more) dates of-interest. For example, you might make a purchase on friday with a debit-card, and it might clear (take effect in your bank account) on-tuesday. It's sometimes useful to model this accurately, so that your-hledger balances match reality. So, you can specify two dates for a-transaction, the *actual* and *effective* date, separated by `=`. Eg:+##### Actual/effective dates - 2010/2/19=2010/2/23 ...+Real-life transactions sometimes have two (or more) dates of interest.+For example, you might buy a movie ticket on friday with a debit or credit+card, and the transaction might appear in your bank account on monday.+When you don't care about this, just record one date. When you do care,+you can record two dates separated by `=`: the *actual date* on the left+and the *effective date* on the right. Here's how hledger and ledger users+use these terms: -Then you can use the `--effective` flag to prefer the effective (second)-date, if any, in reports. This is useful for, eg, seeing a more accurate-daily balance while reconciling a bank account.+ ; The ticket purchase took EFFECT on friday 19th,+ ; but ACTUALly appeared in bank statement on monday 23rd.+ ; The effective date is often the earlier one, but it goes on the right.+ ;+ ; ACTUAL=EFFECTIVE+ 2010/2/23=2010/2/19 movie ticket+ expenses:cinema $10+ assets:bank:checking $-10 -So, what do *actual* and *effective* mean, exactly ? I always assumed that-the actual date comes first, and is "the date you enacted the-transaction", while the effective date comes second, and is optional, and-is "the date the transaction took effect" (showed up in your bank-balance).+You can use the `--effective` flag to prefer the effective date in+reports. This can be useful, eg, to adjust your transaction dates to+match the ones in your bank statement for easier reconciling. -Unfortunately, this is the reverse of c++ ledger's interpretation (cf-[differences](#other-differences)). However it's not *too* serious; just-remember that hledger requires the first date; allows an optional second-date which the `--effective` flag will select; and the meaning of "actual"-and "effective" is up to you.+The year may optionally be omitted in the second date. -The second date may omit the year if it is the same as the first's.+hledger does not allow separate dates for individual postings, unlike c+++ledger. ##### Smart dates @@ -636,7 +638,7 @@ -p "this year to 4/1" If you specify only one date, the missing start or end date will be the-earliest or latest transaction in your ledger data:+earliest or latest transaction in your journal data: -p "from 2009/1/1" (everything after january 1, 2009) -p "from 2009/1" (the same)@@ -715,7 +717,7 @@ Secondly, you can set the price for a commodity as of a certain date, by entering a historical price record. These are lines beginning with "P",-appearing anywhere in the ledger between transactions. Eg, here we say the+appearing anywhere in the journal between transactions. Eg, here we say the exchange rate for 1 euro is $1.35 on 2009/1/1 (and thereafter, until a newer price record is found): @@ -749,12 +751,50 @@ simple reporting of foreign currency transactions, but not for tracking fluctuating-value investments or capital gains. +#### Including other files++You can pull in the content of additional journal files, by writing lines like this:++ !include other/file.journal++The `!include` directive may only be used in journal files, and currently+it may only include other journal files (eg, not timelog files.)++#### Default parent account++You can specify a default parent account within a section of the journal with+the `!account` directive:++ !account home+ + 2010/1/1+ food $10+ cash+ + !end++If `!end` is omitted, the effect lasts to the end of the file.+The above is equivalent to:++ 2010/01/01+ home:food $10+ home:cash $-10++Included files are also affected, eg:++ !account business+ !include biz.journal+ !end+ !account personal+ !include personal.journal+ !end+ #### Timelog reporting hledger will also read timelog files in timeclock.el format. As a convenience, if you invoke hledger via an "hours" symlink or copy, it uses your timelog file (\~/.timelog or $TIMELOG) by default, rather than your-ledger.+journal. Timelog entries look like this: @@ -801,7 +841,7 @@ has introduced additional syntax, which current hledger probably fails to parse. -Generally, it's easy to keep a ledger file that works with both hledger+Generally, it's easy to keep a journal file that works with both hledger and c++ledger if you avoid the more esoteric syntax. Occasionally you'll need to make small edits to restore compatibility for one or the other. @@ -900,4 +940,129 @@ Example: $ chart food --depth 2 -p jan++### Troubleshooting++#### Installation issues++cabal builds a lot of fast-evolving software, and it's not always smooth+sailing. Here are some known issues and things to try:++- **Ask for help on [#hledger](irc://freenode.net/#hledger) or [#haskell](irc://freenode.net/#haskell).**+ Eg: join the #hledger channel with your IRC client and type: "sm: I did ... and ... happened", then leave+ that window open until you get helped.++- **Did you cabal update ?** If you didn't already, ``cabal update`` and try again.++- **Do you have a new enough version of GHC ?** hledger supports GHC 6.10+ and 6.12. Building with the `-fweb` flag requires 6.12 or greater.++- **Do you have a new enough version of cabal-install ?**+ Recent versions tend to be better at resolving dependencies. The error+ `setup: failed to parse output of 'ghc-pkg dump'` is another symptom of+ this. To update, do:+ + $ cabal update+ $ cabal install cabal-install+ $ cabal clean+ + then try installing hledger again.++- **Could not run trhsx.**+ You are installing with `-fweb610`, which needs to run the ``trhsx`` executable.+ It is installed by the hsx package in ~/.cabal/bin, which needs to be in+ your path.++- **Could not run happy.**+ A package (eg haskell-src-exts) needs to run the ``happy`` executable.+ If not using the haskell platform, install the appropriate platform+ package which provides it (eg apt-get install happy).++- <a name="iconv" />**Undefined symbols: ... _iconv ...**+ If cabal gives this error:++ Linking dist/build/hledger/hledger ...+ Undefined symbols:+ "_iconv_close", referenced from:+ _hs_iconv_close in libHSbase-4.2.0.2.a(iconv.o)+ "_iconv", referenced from:+ _hs_iconv in libHSbase-4.2.0.2.a(iconv.o)+ "_iconv_open", referenced from:+ _hs_iconv_open in libHSbase-4.2.0.2.a(iconv.o)++ you are probably on a mac with macports libraries installed, causing+ [this issue](http://hackage.haskell.org/trac/ghc/ticket/4068).+ To work around temporarily, add this --extra-lib-dirs flag:++ $ cabal install hledger --extra-lib-dirs=/usr/lib++ or permanently, add this to ~/.cabal/config:+ + extra-lib-dirs: /usr/lib++- **A ghc: panic! (the 'impossible' happened)** might be+ [this issue](http://hackage.haskell.org/trac/ghc/ticket/3862)++- **Another error while building a hledger package.**+ The current hledger release might have a coding error, or dependency+ error. You could try installing the+ [previous version](http://hackage.haskell.org/package/hledger):++ cabal install hledger-0.x++ or (preferably) the latest development version: install+ [darcs](http://darcs.net) and then:++ darcs get --lazy http://joyful.com/repos/hledger+ cd hledger/hledger-lib+ cabal install+ cd ..+ cabal install [-f...]++- **An error while building non-hledger packages.**+ Resolve these problem packages one at a time. Eg, cabal install pkg1.+ Look for the cause of the failure near the end of the output. If it's+ not apparent, try again with `-v2` or `-v3` for more verbose output.++- **cabal fails to resolve dependencies.**+ It's possible for cabal to get confused, eg if you have+ installed/updated many cabal package versions or GHC itself. You can+ sometimes work around this by using cabal install's `--constraint`+ option. Another (drastic) way is to purge all unnecessary package+ versions by removing (or renaming) ~/.ghc, then trying cabal install+ again.++#### Usage issues++Here are some issues you might encounter when you run hledger:++- <a name="locale" />**hledger: ... hGetContents: invalid argument (Illegal byte sequence)**+ You may get this error when running hledger built with GHC 6.12 on a+ machine using the default C locale, eg a mac:+ + $ locale+ LANG=+ LC_COLLATE="C"+ LC_CTYPE="C"+ LC_MESSAGES="C"+ LC_MONETARY="C"+ LC_NUMERIC="C"+ LC_TIME="C"+ LC_ALL=++ if there is non-ascii text in your journal file:++ $ file my.journal+ .../.journal: UTF-8 Unicode C++ program text+ + In this case you need to set the `LANG` environment variable to a+ locale suitable for the encoding shown (probably UTF-8). You+ can set it temporarily when you run hledger:+ + $ LANG=en_US.UTF-8 hledger ...+ + or permanently:+ + $ echo "export LANG=en_US.UTF-8" >>~/.bash_profile+ $ bash --login
NEWS.rst view
@@ -4,6 +4,37 @@ hledger news ============ +2010/07/17 hledger 0.11+........................++ * split --help, adding --help-options and --help-all/-H, and make it the default command+ * use "journal" instead of "ledger file"; default suffix is .journal, default file is ~/.journal+ * auto-create missing journal files rather than giving an error+ * new format-detecting file reader (mixed journal transactions and timelog entries are no longer supported)+ * work around for first real-world rounding issue (test zero to 8 decimal places instead of 10)+ * when reporting a balancing error, convert the error amount to cost+ * parsing: support double-quoted commodity symbols, containing anything but a newline or double quote+ * parsing: allow minus sign before commodity symbol as well as after (also fixes a convert bug)+ * parsing: fix wrong parse error locations within postings+ * parsing: don't let trailing whitespace in a timelog description mess up account names+ * add: allow blank descriptions+ * balance: --flat provides a simple non-hierarchical format+ * balance: --drop removes leading account name components from a --flat report+ * print, register, balance: fix layout issues with mixed-commodity amounts+ * print: display non-simple commodity names with double-quotes+ * stats: layout tweaks, add payee/description count+ * stats: don't break on an empty file+ * stats: -p/--period support; a reporting interval generates multiple reports+ * test: drop verbose test runner and testpack dependency+ * web: a new web ui based on yesod, requires ghc 6.12; old ghc 6.10-compatible version remains as -fweb610+ * web: allow wiki-like journal editing+ * web: warn and keep running if reloading the journal gives an error+ * web: --port and --base-url options set the webserver's tcp port and base url+ * web: slightly better browser opening on microsoft windows, should find a standard firefox install now+ * web: in a web-enabled build on microsoft windows, run the web ui by default++ Stats: 55 days and 136 commits since last release. Now at 5552 lines of code with 132 tests and 54% unit test coverage.+ 2010/05/23 hledger 0.10 ........................
README.rst view
@@ -20,14 +20,14 @@ Here is a demo_ of the web interface. Here are ready-to-run binaries_ for mac, windows and gnu/linux.-They are not always the `latest release`_, sorry about that.+They are not the `current release`_, sorry about that. Here is the manual_.-For support and more technical info, see `hledger for techies`_ or `email me`_.+For support and more technical info, see `development`_ or `email me`_. .. (If you're reading this in plain text, see also README2, MANUAL etc., or http://hledger.org) -.. _hledger for techies: README2.html+.. _development: README2.html .. _manual: MANUAL.html .. _demo: http://demo.hledger.org .. _binaries: http://hledger.org/binaries/@@ -36,4 +36,4 @@ .. _32 bit intel: http://hledger.org/binaries/hledger-0.6.1+9-linux-i386.gz .. _64 bit intel: http://hledger.org/binaries/hledger-0.6-linux-x86_64.gz .. _email me: mailto:simon@joyful.com-.. _latest release: http://hledger.org/MANUAL.html#installing+.. _current release: http://hledger.org/MANUAL.html#installing
README2.rst view
@@ -1,12 +1,14 @@ ----title: hledger for techies+title: hledger development ----hledger for techies+hledger development =================== +**Techie intro**+ hledger_ is a remix, in haskell_, of John Wiegley's excellent ledger_ accounting tool.-It reads a plain text `ledger file`_ or timelog_ describing your transactions-and displays reports via `command line`_, curses_ or `web interface`_ (click for a demo).+It reads a plain text journal_ or timelog_ file describing your transactions+and displays reports via command line, curses or web interfaces. The hledger project aims to produce: @@ -43,7 +45,7 @@ - chat Simon (sm) on the `#ledger`_ irc channel which we share, or `email me`_ - report problems at `bugs.hledger.org <http://bugs.hledger.org>`_-- share and test ledger snippets at paste . hledger.org+- share and test journal snippets at paste . hledger.org - .. raw:: html <form action="http://groups.google.com/group/hledger/boxsubscribe" >@@ -53,7 +55,7 @@ **Related projects** -- John Wiegley's ledger_ inspired hledger, and we try to stay compatible. You can often use both tools on the same ledger file.+- John Wiegley's ledger_ inspired hledger, and we try to stay compatible. You can often use both tools on the same journal file. - Uwe Hollerbach's umm_ is another haskell tool inspired by h/ledger. - Tim Docker's ledger-reports_ uses hledger as a library to generate `html reports`_. - I have a few older bits and pieces `here <http://joyful.com/Ledger>`_.@@ -65,8 +67,8 @@ .. _hledger: README.html-.. _`ledger file`: http://joyful.com/repos/hledger/sample.ledger-.. _timelog: http://joyful.com/repos/hledger/sample.timelog+.. _journal: http://joyful.com/repos/hledger/data/sample.journal+.. _timelog: http://joyful.com/repos/hledger/data/sample.timelog .. _command line: SCREENSHOTS.html#hledger-screen-1 .. _curses: SCREENSHOTS.html#sshot .. _web interface: http://demo.hledger.org
+ data/sample.journal view
@@ -0,0 +1,40 @@+; A sample journal file.+;+; Sets up this account tree:+; assets+; bank+; checking+; saving+; cash+; expenses+; food+; supplies+; income+; gifts+; salary+; liabilities+; debts++2008/01/01 income+ assets:bank:checking $1+ income:salary++2008/06/01 gift+ assets:bank:checking $1+ income:gifts++2008/06/02 save+ assets:bank:saving $1+ assets:bank:checking++2008/06/03 * eat & shop+ expenses:food $1+ expenses:supplies $1+ assets:cash++2008/12/31 * pay off+ liabilities:debts $1+ assets:bank:checking+++;final comment
− data/sample.ledger
@@ -1,40 +0,0 @@-; A sample ledger file.-;-; Sets up this account tree:-; assets-; bank-; checking-; saving-; cash-; expenses-; food-; supplies-; income-; gifts-; salary-; liabilities-; debts--2008/01/01 income- assets:bank:checking $1- income:salary--2008/06/01 gift- assets:bank:checking $1- income:gifts--2008/06/02 save- assets:bank:saving $1- assets:bank:checking--2008/06/03 * eat & shop- expenses:food $1- expenses:supplies $1- assets:cash--2008/12/31 * pay off- liabilities:debts $1- assets:bank:checking---;final comment
data/sample.rules view
@@ -4,7 +4,7 @@ amount-field 1 currency $ -# account-assigning rules+; account-assigning rules: SPECTRUM expenses:health:gym
data/web/style.css view
@@ -3,13 +3,25 @@ body { font-family: "helvetica","arial", "sans serif"; margin:0; } #navbar { background-color:#eeeeee; border-bottom:2px solid #dddddd; padding:4px 4px 6px 4px; } #navlinks { display:inline; }-.navlink { font-weight:normal; }+.navlink { }+.navlinkcurrent { font-weight:bold; } #searchform { font-size:small; display:inline; margin-left:1em; }-#hledgerorglink { font-size:small; float:right; }-#helplink { font-size:small; margin-left:1em; } #resetlink { font-size:small; }+.toprightlink { font-size:small; margin-left:1em; float:right; } #messages { color:red; background-color:#ffeeee; margin:0.5em;}-#content { padding:0 4px 0 4px; }-#addform { margin-left:1em; font-size:small; float:right;}-#addform table { background-color:#eeeeee; border:2px solid #dddddd; }-#addform #addbuttonrow td { text-align:left; }+.form { margin:1em; font-size:small; }+#addform { background-color:#eeeeee; border:2px solid #dddddd; cell-padding:0; cell-spacing:0; }+#addform { /* float:right; */ }+#addform #descriptionrow { }+#addform #postingrow { }+#addform #addbuttonrow { text-align:right; }+#editform { width:95%; }+#editform textarea { background-color:#eeeeee; font-family:monospace; font-size:medium; width:100%; }+#content { margin:1em; }+.formheading td { padding-bottom:8px; }+#formheading { font-size:medium; font-weight:bold; }+.helprow td { padding-bottom:8px; }+#help {font-style: italic; font-size:smaller; }++/* for -fweb610 */+#hledgerorglink, #helplink { float:right; margin-left:1em; }
hledger.cabal view
@@ -1,9 +1,9 @@ name: hledger-version: 0.10+version: 0.11 category: Finance synopsis: A command-line (or curses or web-based) double-entry accounting tool. description:- hledger reads a plain text ledger file or timelog+ hledger reads a plain text general journal or time log describing your transactions and displays precise balance and register reports via command-line, curses or web interface. It is a remix, in haskell, of John@@ -18,7 +18,7 @@ homepage: http://hledger.org bug-reports: http://code.google.com/p/hledger/issues stability: experimental-tested-with: GHC==6.10+tested-with: GHC==6.10, GHC==6.12 cabal-version: >= 1.2 build-type: Custom data-dir: data@@ -31,26 +31,31 @@ MANUAL.markdown NEWS.rst CONTRIBUTORS.rst- data/sample.ledger+ data/sample.journal data/sample.timelog data/sample.rules +flag chart+ description: enable simple balance pie chart generation+ default: False+ flag vty- description: enable the curses ui+ description: enable the curses-style ui default: False flag web- description: enable the web ui (using simpleserver)+ description: enable the web ui (using yesod/hamlet/simpleserver, requires ghc 6.12) default: False -flag chart- description: enable the pie chart generation+flag web610+ description: enable the web ui (using loli/hsp/simpleserver, works with ghc 6.10) default: False executable hledger main-is: hledger.hs+ -- should set patchlevel here as in Makefile+ cpp-options: -DPATCHLEVEL=0 other-modules:- Paths_hledger Hledger.Cli.Main Hledger.Cli.Options Hledger.Cli.Tests@@ -65,7 +70,7 @@ Hledger.Cli.Commands.Register Hledger.Cli.Commands.Stats build-depends:- hledger-lib >= 0.10+ hledger-lib >= 0.11 ,HUnit ,base >= 3 && < 5 ,containers@@ -79,12 +84,15 @@ ,process ,regexpr >= 0.5.1 ,safe >= 0.2- ,testpack >= 1 && < 2 ,time ,utf8-string >= 0.3 - -- should set patchlevel here as in Makefile- cpp-options: -DPATCHLEVEL=0+ if flag(chart)+ cpp-options: -DCHART+ other-modules:Hledger.Cli.Commands.Chart+ build-depends:+ Chart >= 0.11+ ,colour if flag(vty) cpp-options: -DVTY@@ -96,6 +104,14 @@ cpp-options: -DWEB other-modules:Hledger.Cli.Commands.Web build-depends:+ io-storage >= 0.3 && < 0.4+ ,yesod >= 0.4.0 && < 0.5+ ,convertible-text >= 0.3.0.1++ if flag(web610)+ cpp-options: -DWEB610+ other-modules:Hledger.Cli.Commands.Web610+ build-depends: hsp ,hsx ,xhtml >= 3000.2@@ -107,14 +123,11 @@ ,HTTP >= 4000.0 ,applicative-extras - if flag(chart)- cpp-options: -DCHART- other-modules:Hledger.Cli.Commands.Chart- build-depends:- Chart >= 0.11- ,colour-+-- modules and dependencies below should be as above, except+-- chart, vty, web etc. are not presently exposed as library functions library+ -- should set patchlevel here as in Makefile+ cpp-options: -DPATCHLEVEL=0 exposed-modules: Hledger.Cli.Main Hledger.Cli.Options@@ -130,7 +143,7 @@ Hledger.Cli.Commands.Register Hledger.Cli.Commands.Stats build-depends:- hledger-lib >= 0.10+ hledger-lib >= 0.11 ,HUnit ,base >= 3 && < 5 ,containers@@ -144,40 +157,8 @@ ,process ,regexpr >= 0.5.1 ,safe >= 0.2- ,testpack >= 1 && < 2 ,time ,utf8-string >= 0.3-- -- should set patchlevel here as in Makefile- cpp-options: -DPATCHLEVEL=0-- if flag(vty)- cpp-options: -DVTY- exposed-modules:Hledger.Cli.Commands.Vty- build-depends:- vty >= 4.0.0.1-- if flag(web)- cpp-options: -DWEB- exposed-modules:Hledger.Cli.Commands.Web- build-depends:- hsp- ,hsx- ,xhtml >= 3000.2- ,loli- ,io-storage- ,hack-contrib- ,hack- ,hack-handler-simpleserver- ,HTTP >= 4000.0- ,applicative-extras-- if flag(chart)- cpp-options: -DCHART- exposed-modules:Hledger.Cli.Commands.Chart- build-depends:- Chart >= 0.11- ,colour -- source-repository head -- type: darcs