packages feed

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 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 "&nbsp;"+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 "&nbsp;"
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