2022-08-17 13:04:50 +03:00
|
|
|
-- TODO: brick 1 support
|
|
|
|
-- https://hackage.haskell.org/package/brick-1.0/changelog
|
2010-11-25 10:10:38 +03:00
|
|
|
{-|
|
2015-08-13 21:22:40 +03:00
|
|
|
hledger-ui - a hledger add-on providing a curses-style interface.
|
2015-08-18 03:44:18 +03:00
|
|
|
Copyright (c) 2007-2015 Simon Michael <simon@joyful.com>
|
2010-11-25 10:10:38 +03:00
|
|
|
Released under GPL version 3 or later.
|
2008-12-08 20:27:16 +03:00
|
|
|
-}
|
2021-08-27 13:58:02 +03:00
|
|
|
{-# LANGUAGE LambdaCase #-}
|
2015-08-18 03:44:18 +03:00
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
2021-05-09 03:20:42 +03:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2008-12-08 20:27:16 +03:00
|
|
|
|
2015-08-18 03:44:18 +03:00
|
|
|
module Hledger.UI.Main where
|
2010-11-25 10:10:38 +03:00
|
|
|
|
2021-08-04 04:55:42 +03:00
|
|
|
import Control.Applicative ((<|>))
|
2017-01-14 21:41:46 +03:00
|
|
|
import Control.Concurrent (threadDelay)
|
2021-03-01 14:35:21 +03:00
|
|
|
import Control.Concurrent.Async (withAsync)
|
|
|
|
import Control.Monad (forM_, void, when)
|
|
|
|
import Data.List (find)
|
2020-01-04 09:09:01 +03:00
|
|
|
import Data.List.Extra (nubSort)
|
2021-03-01 14:35:21 +03:00
|
|
|
import Data.Maybe (fromMaybe)
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
import qualified Data.Text as T
|
2021-11-18 05:56:50 +03:00
|
|
|
import Graphics.Vty (mkVty, Mode (Mouse), Vty (outputIface), Output (setMode))
|
2021-08-30 05:09:47 +03:00
|
|
|
import Lens.Micro ((^.))
|
2021-03-01 14:35:21 +03:00
|
|
|
import System.Directory (canonicalizePath)
|
|
|
|
import System.FilePath (takeDirectory)
|
|
|
|
import System.FSNotify (Event(Modified), isPollingManager, watchDir, withManager)
|
2015-08-20 14:54:23 +03:00
|
|
|
import Brick
|
2015-08-18 03:44:18 +03:00
|
|
|
|
2017-01-14 21:41:46 +03:00
|
|
|
import qualified Brick.BChan as BC
|
|
|
|
|
2011-07-18 03:05:56 +04:00
|
|
|
import Hledger
|
2017-09-04 18:06:25 +03:00
|
|
|
import Hledger.Cli hiding (progname,prognameandversion)
|
2015-08-28 22:33:33 +03:00
|
|
|
import Hledger.UI.UIOptions
|
2015-08-18 03:44:18 +03:00
|
|
|
import Hledger.UI.UITypes
|
2015-08-23 03:46:57 +03:00
|
|
|
import Hledger.UI.Theme
|
2016-06-08 21:03:49 +03:00
|
|
|
import Hledger.UI.AccountsScreen
|
|
|
|
import Hledger.UI.RegisterScreen
|
2022-08-23 01:55:14 +03:00
|
|
|
import Hledger.UI.UIUtils (dlogUiTrace)
|
2010-11-25 10:10:38 +03:00
|
|
|
|
2015-08-18 03:44:18 +03:00
|
|
|
----------------------------------------------------------------------
|
|
|
|
|
2017-01-14 21:41:46 +03:00
|
|
|
newChan :: IO (BC.BChan a)
|
|
|
|
newChan = BC.newBChan 10
|
|
|
|
|
|
|
|
writeChan :: BC.BChan a -> a -> IO ()
|
|
|
|
writeChan = BC.writeBChan
|
|
|
|
|
|
|
|
|
2010-11-25 10:10:38 +03:00
|
|
|
main :: IO ()
|
|
|
|
main = do
|
2021-08-26 17:09:07 +03:00
|
|
|
opts@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=iopts,rawopts_=rawopts}} <- getHledgerUIOpts
|
2015-08-12 05:08:33 +03:00
|
|
|
-- when (debug_ $ cliopts_ opts) $ printf "%s\n" prognameandversion >> printf "opts: %s\n" (show opts)
|
2020-02-22 22:02:30 +03:00
|
|
|
|
2020-12-12 22:51:58 +03:00
|
|
|
-- always generate forecasted periodic transactions; their visibility will be toggled by the UI.
|
2021-08-04 04:55:42 +03:00
|
|
|
let copts' = copts{inputopts_=iopts{forecast_=forecast_ iopts <|> Just nulldatespan}}
|
2020-02-22 22:02:30 +03:00
|
|
|
|
|
|
|
case True of
|
2020-02-22 22:04:32 +03:00
|
|
|
_ | "help" `inRawOpts` rawopts -> putStr (showModeUsage uimode)
|
2021-03-03 16:52:57 +03:00
|
|
|
_ | "info" `inRawOpts` rawopts -> runInfoForTopic "hledger-ui" Nothing
|
|
|
|
_ | "man" `inRawOpts` rawopts -> runManForTopic "hledger-ui" Nothing
|
2020-02-22 22:04:32 +03:00
|
|
|
_ | "version" `inRawOpts` rawopts -> putStrLn prognameandversion
|
2021-08-06 10:51:03 +03:00
|
|
|
-- _ | "binary-filename" `inRawOpts` rawopts -> putStrLn (binaryfilename progname)
|
2020-02-22 22:02:30 +03:00
|
|
|
_ -> withJournalDo copts' (runBrickUi opts)
|
2008-12-08 20:27:16 +03:00
|
|
|
|
2015-08-18 03:44:18 +03:00
|
|
|
runBrickUi :: UIOpts -> Journal -> IO ()
|
2022-08-23 01:55:14 +03:00
|
|
|
runBrickUi uopts@UIOpts{uoCliOpts=copts@CliOpts{inputopts_=_iopts,reportspec_=rspec@ReportSpec{_rsReportOpts=ropts}}} j =
|
|
|
|
dlogUiTrace "========= runBrickUi" $ do
|
2015-08-18 03:44:18 +03:00
|
|
|
let
|
2021-08-30 05:09:47 +03:00
|
|
|
today = copts^.rsDay
|
2015-09-04 06:40:43 +03:00
|
|
|
|
2020-11-15 22:20:40 +03:00
|
|
|
-- hledger-ui's query handling is currently in flux, mixing old and new approaches.
|
|
|
|
-- Related: #1340, #1383, #1387. Some notes and terminology:
|
|
|
|
|
|
|
|
-- The *startup query* is the Query generated at program startup, from
|
|
|
|
-- command line options, arguments, and the current date. hledger CLI
|
|
|
|
-- uses this.
|
|
|
|
|
|
|
|
-- hledger-ui/hledger-web allow the query to be changed at will, creating
|
|
|
|
-- a new *runtime query* each time.
|
|
|
|
|
|
|
|
-- The startup query or part of it can be used as a *constraint query*,
|
|
|
|
-- limiting all runtime queries. hledger-web does this with the startup
|
|
|
|
-- report period, never showing transactions outside those dates.
|
|
|
|
-- hledger-ui does not do this.
|
|
|
|
|
|
|
|
-- A query is a combination of multiple subqueries/terms, which are
|
|
|
|
-- generated from command line options and arguments, ui/web app runtime
|
|
|
|
-- state, and/or the current date.
|
|
|
|
|
|
|
|
-- Some subqueries are generated by parsing freeform user input, which
|
|
|
|
-- can fail. We don't want hledger users to see such failures except:
|
|
|
|
|
|
|
|
-- 1. at program startup, in which case the program exits
|
|
|
|
-- 2. after entering a new freeform query in hledger-ui/web, in which case
|
|
|
|
-- the change is rejected and the program keeps running
|
|
|
|
|
|
|
|
-- So we should parse those kinds of subquery only at those times. Any
|
|
|
|
-- subqueries which do not require parsing can be kept separate. And
|
|
|
|
-- these can be combined to make the full query when needed, eg when
|
|
|
|
-- hledger-ui screens are generating their data. (TODO)
|
|
|
|
|
2020-11-15 22:32:43 +03:00
|
|
|
-- Some parts of the query are also kept separate for UI reasons.
|
|
|
|
-- hledger-ui provides special UI for controlling depth (number keys),
|
|
|
|
-- the report period (shift arrow keys), realness/status filters (RUPC keys) etc.
|
|
|
|
-- There is also a freeform text area for extra query terms (/ key).
|
|
|
|
-- It's cleaner and less conflicting to keep the former out of the latter.
|
2020-11-15 22:20:40 +03:00
|
|
|
|
2015-09-04 06:40:43 +03:00
|
|
|
uopts' = uopts{
|
2021-08-26 17:09:07 +03:00
|
|
|
uoCliOpts=copts{
|
2020-09-16 04:45:52 +03:00
|
|
|
reportspec_=rspec{
|
2021-07-23 09:47:48 +03:00
|
|
|
_rsQuery=filteredQuery $ _rsQuery rspec, -- query with depth/date parts removed
|
|
|
|
_rsReportOpts=ropts{
|
|
|
|
depth_ =queryDepth $ _rsQuery rspec, -- query's depth part
|
2020-11-15 22:20:40 +03:00
|
|
|
period_=periodfromoptsandargs, -- query's date part
|
|
|
|
no_elide_=True, -- avoid squashing boring account names, for a more regular tree (unlike hledger)
|
2021-08-08 16:01:07 +03:00
|
|
|
empty_=not $ empty_ ropts -- show zero items by default, hide them with -E (unlike hledger)
|
2020-09-16 04:45:52 +03:00
|
|
|
}
|
2015-09-04 06:40:43 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
where
|
2021-07-23 09:47:48 +03:00
|
|
|
datespanfromargs = queryDateSpan (date2_ ropts) $ _rsQuery rspec
|
2016-08-10 20:22:09 +03:00
|
|
|
periodfromoptsandargs =
|
|
|
|
dateSpanAsPeriod $ spansIntersect [periodAsDateSpan $ period_ ropts, datespanfromargs]
|
2020-09-03 10:12:36 +03:00
|
|
|
filteredQuery q = simplifyQuery $ And [queryFromFlags ropts, filtered q]
|
|
|
|
where filtered = filterQuery (\x -> not $ queryIsDepth x || queryIsDate x)
|
2015-09-04 06:40:43 +03:00
|
|
|
|
2021-08-26 15:39:14 +03:00
|
|
|
(scr, prevscrs) = case uoRegister uopts' of
|
2016-06-08 21:03:49 +03:00
|
|
|
Nothing -> (accountsScreen, [])
|
2015-09-04 06:40:43 +03:00
|
|
|
-- with --register, start on the register screen, and also put
|
|
|
|
-- the accounts screen on the prev screens stack so you can exit
|
|
|
|
-- to that as usual.
|
2016-07-27 03:28:29 +03:00
|
|
|
Just apat -> (rsSetAccount acct False registerScreen, [ascr'])
|
2015-08-28 18:48:40 +03:00
|
|
|
where
|
2021-03-01 14:35:21 +03:00
|
|
|
acct = fromMaybe (error' $ "--register "++apat++" did not match any account") -- PARTIAL:
|
|
|
|
. firstMatch $ journalAccountNamesDeclaredOrImplied j
|
|
|
|
firstMatch = case toRegexCI $ T.pack apat of
|
|
|
|
Right re -> find (regexMatchText re)
|
|
|
|
Left _ -> const Nothing
|
2015-09-04 06:40:43 +03:00
|
|
|
-- Initialising the accounts screen is awkward, requiring
|
2016-06-11 03:30:45 +03:00
|
|
|
-- another temporary UIState value..
|
2015-09-04 06:40:43 +03:00
|
|
|
ascr' = aScreen $
|
2021-08-30 05:09:47 +03:00
|
|
|
asInit today True
|
2018-02-18 19:05:33 +03:00
|
|
|
UIState{
|
2020-07-18 22:24:37 +03:00
|
|
|
astartupopts=uopts'
|
|
|
|
,aopts=uopts'
|
2018-02-18 19:05:33 +03:00
|
|
|
,ajournal=j
|
|
|
|
,aScreen=asSetSelectedAccount acct accountsScreen
|
|
|
|
,aPrevScreens=[]
|
|
|
|
,aMode=Normal
|
|
|
|
}
|
2017-01-14 21:41:46 +03:00
|
|
|
|
2016-11-24 22:32:13 +03:00
|
|
|
ui =
|
2021-08-30 05:09:47 +03:00
|
|
|
(sInit scr) today True $
|
2018-02-18 19:05:33 +03:00
|
|
|
UIState{
|
2020-07-18 22:24:37 +03:00
|
|
|
astartupopts=uopts'
|
|
|
|
,aopts=uopts'
|
2018-02-18 19:05:33 +03:00
|
|
|
,ajournal=j
|
|
|
|
,aScreen=scr
|
|
|
|
,aPrevScreens=prevscrs
|
|
|
|
,aMode=Normal
|
|
|
|
}
|
|
|
|
|
|
|
|
brickapp :: App UIState AppEvent Name
|
2016-06-07 17:04:32 +03:00
|
|
|
brickapp = App {
|
2022-08-17 13:04:50 +03:00
|
|
|
appStartEvent = return ()
|
2021-08-26 15:39:14 +03:00
|
|
|
, appAttrMap = const $ fromMaybe defaultTheme $ getTheme =<< uoTheme uopts'
|
2015-08-20 14:54:23 +03:00
|
|
|
, appChooseCursor = showFirstCursor
|
2022-08-23 01:58:29 +03:00
|
|
|
, appHandleEvent = \ev -> do ui <- get; sHandle (aScreen ui) ev
|
|
|
|
, appDraw = \ui -> sDraw (aScreen ui) ui
|
2015-08-18 03:44:18 +03:00
|
|
|
}
|
|
|
|
|
2018-10-17 17:25:56 +03:00
|
|
|
-- print (length (show ui)) >> exitSuccess -- show any debug output to this point & quit
|
2019-07-15 13:28:52 +03:00
|
|
|
|
2021-11-18 05:56:50 +03:00
|
|
|
let
|
|
|
|
-- helper: make a Vty terminal controller with mouse support enabled
|
|
|
|
makevty = do
|
|
|
|
v <- mkVty mempty
|
|
|
|
setMode (outputIface v) Mouse True
|
|
|
|
return v
|
|
|
|
|
2021-08-26 17:09:07 +03:00
|
|
|
if not (uoWatch uopts')
|
2021-11-18 05:56:50 +03:00
|
|
|
then do
|
|
|
|
vty <- makevty
|
|
|
|
void $ customMain vty makevty Nothing brickapp ui
|
2016-11-25 17:59:09 +03:00
|
|
|
|
2016-12-02 06:26:17 +03:00
|
|
|
else do
|
|
|
|
-- a channel for sending misc. events to the app
|
|
|
|
eventChan <- newChan
|
|
|
|
|
|
|
|
-- start a background thread reporting changes in the current date
|
|
|
|
-- use async for proper child termination in GHCI
|
|
|
|
let
|
2016-12-03 02:36:23 +03:00
|
|
|
watchDate old = do
|
2016-12-31 07:00:50 +03:00
|
|
|
threadDelay 1000000 -- 1 s
|
2016-12-03 02:36:23 +03:00
|
|
|
new <- getCurrentDay
|
|
|
|
when (new /= old) $ do
|
|
|
|
let dc = DateChange old new
|
|
|
|
-- dbg1IO "datechange" dc -- XXX don't uncomment until dbg*IO fixed to use traceIO, GHC may block/end thread
|
2016-12-31 07:00:50 +03:00
|
|
|
-- traceIO $ show dc
|
2016-12-03 02:36:23 +03:00
|
|
|
writeChan eventChan dc
|
|
|
|
watchDate new
|
2016-12-02 06:26:17 +03:00
|
|
|
|
2021-08-30 05:09:47 +03:00
|
|
|
withAsync
|
2021-07-20 02:18:20 +03:00
|
|
|
-- run this small task asynchronously:
|
2021-08-30 05:09:47 +03:00
|
|
|
(getCurrentDay >>= watchDate)
|
2021-07-20 02:18:20 +03:00
|
|
|
-- until this main task terminates:
|
|
|
|
$ \_async ->
|
2016-12-02 06:26:17 +03:00
|
|
|
-- start one or more background threads reporting changes in the directories of our files
|
2016-12-31 07:14:58 +03:00
|
|
|
-- XXX many quick successive saves causes the problems listed in BUGS
|
|
|
|
-- with Debounce increased to 1s it easily gets stuck on an error or blank screen
|
|
|
|
-- until you press g, but it becomes responsive again quickly.
|
|
|
|
-- withManagerConf defaultConfig{confDebounce=Debounce 1} $ \mgr -> do
|
|
|
|
-- with Debounce at the default 1ms it clears transient errors itself
|
|
|
|
-- but gets tied up for ages
|
2016-12-02 06:26:17 +03:00
|
|
|
withManager $ \mgr -> do
|
|
|
|
dbg1IO "fsnotify using polling ?" $ isPollingManager mgr
|
2018-02-18 19:05:33 +03:00
|
|
|
files <- mapM (canonicalizePath . fst) $ jfiles j
|
2020-01-04 09:09:01 +03:00
|
|
|
let directories = nubSort $ map takeDirectory files
|
2016-12-02 06:26:17 +03:00
|
|
|
dbg1IO "files" files
|
|
|
|
dbg1IO "directories to watch" directories
|
|
|
|
|
|
|
|
forM_ directories $ \d -> watchDir
|
|
|
|
mgr
|
|
|
|
d
|
|
|
|
-- predicate: ignore changes not involving our files
|
2021-08-27 13:58:02 +03:00
|
|
|
(\case
|
2021-05-09 03:12:29 +03:00
|
|
|
Modified f _ False -> f `elem` files
|
2016-12-31 07:00:50 +03:00
|
|
|
-- Added f _ -> f `elem` files
|
|
|
|
-- Removed f _ -> f `elem` files
|
|
|
|
-- we don't handle adding/removing journal files right now
|
|
|
|
-- and there might be some of those events from tmp files
|
|
|
|
-- clogging things up so let's ignore them
|
|
|
|
_ -> False
|
2016-12-02 06:26:17 +03:00
|
|
|
)
|
|
|
|
-- action: send event to app
|
|
|
|
(\fev -> do
|
|
|
|
-- return $ dbglog "fsnotify" $ showFSNEvent fev -- not working
|
2018-06-05 16:41:13 +03:00
|
|
|
dbg1IO "fsnotify" $ show fev
|
2016-12-02 06:26:17 +03:00
|
|
|
writeChan eventChan FileChange
|
|
|
|
)
|
|
|
|
|
2021-11-18 05:56:50 +03:00
|
|
|
-- and start the app. Must be inside the withManager block. (XXX makevty too ?)
|
|
|
|
vty <- makevty
|
|
|
|
void $ customMain vty makevty (Just eventChan) brickapp ui
|