ledger-report.el 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. ;;; ledger-report.el --- Helper code for use with the "ledger" command-line tool
  2. ;; Copyright (C) 2003-2016 John Wiegley (johnw AT gnu DOT org)
  3. ;; This file is not part of GNU Emacs.
  4. ;; This is free software; you can redistribute it and/or modify it under
  5. ;; the terms of the GNU General Public License as published by the Free
  6. ;; Software Foundation; either version 2, or (at your option) any later
  7. ;; version.
  8. ;;
  9. ;; This is distributed in the hope that it will be useful, but WITHOUT
  10. ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11. ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. ;; for more details.
  13. ;;
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs; see the file COPYING. If not, write to the
  16. ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. ;; MA 02110-1301 USA.
  18. ;;; Commentary:
  19. ;; Provide facilities for running and saving reports in emacs
  20. ;;; Code:
  21. (require 'easymenu)
  22. (eval-when-compile
  23. (require 'cl))
  24. (defgroup ledger-report nil
  25. "Customization option for the Report buffer"
  26. :group 'ledger)
  27. (defcustom ledger-reports
  28. '(("bal" "ledger -f %(ledger-file) bal")
  29. ("reg" "ledger -f %(ledger-file) reg")
  30. ("payee" "ledger -f %(ledger-file) reg @%(payee)")
  31. ("account" "ledger -f %(ledger-file) reg %(account)"))
  32. "Definition of reports to run.
  33. Each element has the form (NAME CMDLINE). The command line can
  34. contain format specifiers that are replaced with context sensitive
  35. information. Format specifiers have the format '%(<name>)' where
  36. <name> is an identifier for the information to be replaced. The
  37. `ledger-report-format-specifiers' alist variable contains a mapping
  38. from format specifier identifier to a Lisp function that implements
  39. the substitution. See the documentation of the individual functions
  40. in that variable for more information on the behavior of each
  41. specifier."
  42. :type '(repeat (list (string :tag "Report Name")
  43. (string :tag "Command Line")))
  44. :group 'ledger-report)
  45. (defcustom ledger-report-format-specifiers
  46. '(("ledger-file" . ledger-report-ledger-file-format-specifier)
  47. ("payee" . ledger-report-payee-format-specifier)
  48. ("account" . ledger-report-account-format-specifier)
  49. ("tagname" . ledger-report-tagname-format-specifier)
  50. ("tagvalue" . ledger-report-tagvalue-format-specifier))
  51. "An alist mapping ledger report format specifiers to implementing functions.
  52. The function is called with no parameters and expected to return the
  53. text that should replace the format specifier."
  54. :type 'alist
  55. :group 'ledger-report)
  56. (defcustom ledger-report-auto-refresh t
  57. "If t then automatically rerun the report when the ledger buffer is saved."
  58. :type 'boolean
  59. :group 'ledger-report)
  60. (defcustom ledger-report-auto-refresh-sticky-cursor nil
  61. "If t then try to place cursor at same relative position as it was before auto-refresh."
  62. :type 'boolean
  63. :group 'ledger-report)
  64. (defvar ledger-report-buffer-name "*Ledger Report*")
  65. (defvar ledger-report-name nil)
  66. (defvar ledger-report-cmd nil)
  67. (defvar ledger-report-name-prompt-history nil)
  68. (defvar ledger-report-cmd-prompt-history nil)
  69. (defvar ledger-original-window-cfg nil)
  70. (defvar ledger-report-saved nil)
  71. (defvar ledger-minibuffer-history nil)
  72. (defvar ledger-report-mode-abbrev-table)
  73. (defvar ledger-report-is-reversed nil)
  74. (defvar ledger-report-cursor-line-number nil)
  75. (defun ledger-report-reverse-report ()
  76. "Reverse the order of the report."
  77. (interactive)
  78. (ledger-report-reverse-lines)
  79. (setq ledger-report-is-reversed (not ledger-report-is-reversed)))
  80. (defun ledger-report-reverse-lines ()
  81. (goto-char (point-min))
  82. (forward-paragraph)
  83. (forward-line)
  84. (save-excursion
  85. (setq inhibit-read-only t)
  86. (reverse-region (point) (point-max))))
  87. (defvar ledger-report-mode-map
  88. (let ((map (make-sparse-keymap)))
  89. (define-key map [? ] 'scroll-up)
  90. (define-key map [backspace] 'scroll-down)
  91. (define-key map [?r] 'ledger-report-redo)
  92. (define-key map [(shift ?r)] 'ledger-report-reverse-report)
  93. (define-key map [?s] 'ledger-report-save)
  94. (define-key map [?k] 'ledger-report-kill)
  95. (define-key map [?e] 'ledger-report-edit-report)
  96. (define-key map [( shift ?e)] 'ledger-report-edit-reports)
  97. (define-key map [?q] 'ledger-report-quit)
  98. (define-key map [?g] 'ledger-report-redo)
  99. (define-key map [(control ?c) (control ?l) (control ?r)]
  100. 'ledger-report-redo)
  101. (define-key map [(control ?c) (control ?l) (control ?S)]
  102. 'ledger-report-save)
  103. (define-key map [(control ?c) (control ?l) (control ?k)]
  104. 'ledger-report-kill)
  105. (define-key map [(control ?c) (control ?l) (control ?e)]
  106. 'ledger-report-edit)
  107. (define-key map [return] 'ledger-report-visit-source)
  108. map)
  109. "Keymap for `ledger-report-mode'.")
  110. (easy-menu-define ledger-report-mode-menu ledger-report-mode-map
  111. "Ledger report menu"
  112. '("Reports"
  113. ["Save Report" ledger-report-save]
  114. ["Edit Current Report" ledger-report-edit-report]
  115. ["Edit All Reports" ledger-report-edit-reports]
  116. ["Re-run Report" ledger-report-redo]
  117. "---"
  118. ["Reverse report order" ledger-report-reverse-report]
  119. "---"
  120. ["Scroll Up" scroll-up]
  121. ["Visit Source" ledger-report-visit-source]
  122. ["Scroll Down" scroll-down]
  123. "---"
  124. ["Quit" ledger-report-quit]
  125. ))
  126. (define-derived-mode ledger-report-mode text-mode "Ledger-Report"
  127. "A mode for viewing ledger reports.")
  128. (defun ledger-report-tagname-format-specifier ()
  129. "Return a valid meta-data tag name"
  130. ;; It is intended completion should be available on existing account
  131. ;; names, but it remains to be implemented.
  132. (ledger-read-string-with-default "Tag Name: " nil))
  133. (defun ledger-report-tagvalue-format-specifier ()
  134. "Return a valid meta-data tag name"
  135. ;; It is intended completion should be available on existing account
  136. ;; names, but it remains to be implemented.
  137. (ledger-read-string-with-default "Tag Value: " nil))
  138. (defun ledger-report-read-name ()
  139. "Read the name of a ledger report to use, with completion.
  140. The empty string and unknown names are allowed."
  141. (completing-read "Report name: "
  142. ledger-reports nil nil nil
  143. 'ledger-report-name-prompt-history nil))
  144. (defun ledger-report (report-name edit)
  145. "Run a user-specified report from `ledger-reports'.
  146. Prompts the user for the REPORT-NAME of the report to run or
  147. EDIT. If no name is entered, the user will be prompted for a
  148. command line to run. The command line specified or associated
  149. with the selected report name is run and the output is made
  150. available in another buffer for viewing. If a prefix argument is
  151. given and the user selects a valid report name, the user is
  152. prompted with the corresponding command line for editing before
  153. the command is run.
  154. The output buffer will be in `ledger-report-mode', which defines
  155. commands for saving a new named report based on the command line
  156. used to generate the buffer, navigating the buffer, etc."
  157. (interactive
  158. (progn
  159. (when (and (buffer-modified-p)
  160. (y-or-n-p "Buffer modified, save it? "))
  161. (save-buffer))
  162. (let ((rname (ledger-report-read-name))
  163. (edit (not (null current-prefix-arg))))
  164. (list rname edit))))
  165. (let ((buf (current-buffer))
  166. (rbuf (get-buffer ledger-report-buffer-name))
  167. (wcfg (current-window-configuration)))
  168. (if rbuf
  169. (kill-buffer rbuf))
  170. (with-current-buffer
  171. (pop-to-buffer (get-buffer-create ledger-report-buffer-name))
  172. (ledger-report-mode)
  173. (set (make-local-variable 'ledger-report-saved) nil)
  174. (set (make-local-variable 'ledger-buf) buf)
  175. (set (make-local-variable 'ledger-report-name) report-name)
  176. (set (make-local-variable 'ledger-original-window-cfg) wcfg)
  177. (set (make-local-variable 'ledger-report-is-reversed) nil)
  178. (ledger-do-report (ledger-report-cmd report-name edit))
  179. (shrink-window-if-larger-than-buffer)
  180. (set-buffer-modified-p nil)
  181. (setq buffer-read-only t)
  182. (message "q to quit; r to redo; e to edit; k to kill; s to save; SPC and DEL to scroll"))))
  183. (defun ledger-report-string-empty-p (s)
  184. "Check S for the empty string."
  185. (string-equal "" s))
  186. (defun ledger-report-name-exists (name)
  187. "Check to see if the given report NAME exists.
  188. If name exists, returns the object naming the report,
  189. otherwise returns nil."
  190. (unless (ledger-report-string-empty-p name)
  191. (car (assoc name ledger-reports))))
  192. (defun ledger-reports-add (name cmd)
  193. "Add a new report NAME and CMD to `ledger-reports'."
  194. (setq ledger-reports (cons (list name cmd) ledger-reports)))
  195. (defun ledger-reports-custom-save ()
  196. "Save the `ledger-reports' variable using the customize framework."
  197. (customize-save-variable 'ledger-reports ledger-reports))
  198. (defun ledger-report-read-command (report-cmd)
  199. "Read the command line to create a report from REPORT-CMD."
  200. (read-from-minibuffer "Report command line: "
  201. (if (null report-cmd) "ledger " report-cmd)
  202. nil nil 'ledger-report-cmd-prompt-history))
  203. (defun ledger-report-ledger-file-format-specifier ()
  204. "Substitute the full path to master or current ledger file.
  205. The master file name is determined by the variable `ledger-master-file'
  206. buffer-local variable which can be set using file variables.
  207. If it is set, it is used, otherwise the current buffer file is
  208. used."
  209. (ledger-master-file))
  210. ;; General helper functions
  211. (defvar ledger-master-file nil)
  212. (defun ledger-master-file ()
  213. "Return the master file for a ledger file.
  214. The master file is either the file for the current ledger buffer or the
  215. file specified by the buffer-local variable `ledger-master-file'. Typically
  216. this variable would be set in a file local variable comment block at the
  217. end of a ledger file which is included in some other file."
  218. (if ledger-master-file
  219. (expand-file-name ledger-master-file)
  220. (buffer-file-name)))
  221. (defun ledger-report-payee-format-specifier ()
  222. "Substitute a payee name.
  223. The user is prompted to enter a payee and that is substitued. If
  224. point is in an xact, the payee for that xact is used as the
  225. default."
  226. ;; It is intended completion should be available on existing
  227. ;; payees, but the list of possible completions needs to be
  228. ;; developed to allow this.
  229. (ledger-read-string-with-default "Payee" (regexp-quote (ledger-xact-payee))))
  230. (defun ledger-report-account-format-specifier ()
  231. "Substitute an account name.
  232. The user is prompted to enter an account name, which can be any
  233. regular expression identifying an account. If point is on an account
  234. posting line for an xact, the full account name on that line is
  235. the default."
  236. ;; It is intended completion should be available on existing account
  237. ;; names, but it remains to be implemented.
  238. (ledger-read-account-with-prompt "Account"))
  239. (defun ledger-report-expand-format-specifiers (report-cmd)
  240. "Expand %(account) and %(payee) appearing in REPORT-CMD with thing under point."
  241. (save-match-data
  242. (let ((expanded-cmd report-cmd))
  243. (set-match-data (list 0 0))
  244. (while (string-match "%(\\([^)]*\\))" expanded-cmd (if (> (length expanded-cmd) (match-end 0))
  245. (match-end 0)
  246. (1- (length expanded-cmd))))
  247. (let* ((specifier (match-string 1 expanded-cmd))
  248. (f (cdr (assoc specifier ledger-report-format-specifiers))))
  249. (if f
  250. (setq expanded-cmd (replace-match
  251. (save-match-data
  252. (with-current-buffer ledger-buf
  253. (shell-quote-argument (funcall f))))
  254. t t expanded-cmd)))))
  255. expanded-cmd)))
  256. (defun ledger-report-cmd (report-name edit)
  257. "Get the command line to run the report name REPORT-NAME.
  258. Optional EDIT the command."
  259. (let ((report-cmd (car (cdr (assoc report-name ledger-reports)))))
  260. ;; logic for substitution goes here
  261. (when (or (null report-cmd) edit)
  262. (setq report-cmd (ledger-report-read-command report-cmd))
  263. (setq ledger-report-saved nil)) ;; this is a new report, or edited report
  264. (setq report-cmd (ledger-report-expand-format-specifiers report-cmd))
  265. (set (make-local-variable 'ledger-report-cmd) report-cmd)
  266. (or (ledger-report-string-empty-p report-name)
  267. (ledger-report-name-exists report-name)
  268. (progn
  269. (ledger-reports-add report-name report-cmd)
  270. (ledger-reports-custom-save)))
  271. report-cmd))
  272. (defun ledger-do-report (cmd)
  273. "Run a report command line CMD."
  274. (goto-char (point-min))
  275. (insert (format "Report: %s\n" ledger-report-name)
  276. (format "Command: %s\n" cmd)
  277. (make-string (- (window-width) 1) ?=)
  278. "\n\n")
  279. (let ((data-pos (point))
  280. (register-report (string-match " reg\\(ister\\)? " cmd))
  281. files-in-report)
  282. (shell-command
  283. ;; --subtotal does not produce identifiable transactions, so don't
  284. ;; prepend location information for them
  285. (if (and register-report
  286. (not (string-match "--subtotal" cmd)))
  287. (concat cmd " --prepend-format='%(filename):%(beg_line):'")
  288. cmd)
  289. t nil)
  290. (when register-report
  291. (goto-char data-pos)
  292. (while (re-search-forward "^\\(/[^:]+\\)?:\\([0-9]+\\)?:" nil t)
  293. (let ((file (match-string 1))
  294. (line (string-to-number (match-string 2))))
  295. (delete-region (match-beginning 0) (match-end 0))
  296. (when file
  297. (set-text-properties (line-beginning-position) (line-end-position)
  298. (list 'ledger-source (cons file (save-window-excursion
  299. (save-excursion
  300. (find-file file)
  301. (widen)
  302. (ledger-navigate-to-line line)
  303. (point-marker))))))
  304. (add-text-properties (line-beginning-position) (line-end-position)
  305. (list 'face 'ledger-font-report-clickable-face))
  306. (end-of-line)))))
  307. (goto-char data-pos)))
  308. (defun ledger-report-visit-source ()
  309. "Visit the transaction under point in the report window."
  310. (interactive)
  311. (let* ((prop (get-text-property (point) 'ledger-source))
  312. (file (if prop (car prop)))
  313. (line-or-marker (if prop (cdr prop))))
  314. (when (and file line-or-marker)
  315. (find-file-other-window file)
  316. (widen)
  317. (if (markerp line-or-marker)
  318. (goto-char line-or-marker)
  319. (goto-char (point-min))
  320. (forward-line (1- line-or-marker))
  321. (re-search-backward "^[0-9]+")
  322. (beginning-of-line)
  323. (let ((start-of-txn (point)))
  324. (forward-paragraph)
  325. (narrow-to-region start-of-txn (point))
  326. (backward-paragraph))))))
  327. (defun ledger-report-goto ()
  328. "Goto the ledger report buffer."
  329. (interactive)
  330. (let ((rbuf (get-buffer ledger-report-buffer-name)))
  331. (if (not rbuf)
  332. (error "There is no ledger report buffer"))
  333. (pop-to-buffer rbuf)
  334. (shrink-window-if-larger-than-buffer)))
  335. (defun ledger-report-redo ()
  336. "Redo the report in the current ledger report buffer."
  337. (interactive)
  338. (let ((cur-buf (current-buffer)))
  339. (if (and ledger-report-auto-refresh
  340. (or (string= (format-mode-line 'mode-name) "Ledger")
  341. (string= (format-mode-line 'mode-name) "Ledger-Report"))
  342. (get-buffer ledger-report-buffer-name))
  343. (progn
  344. (pop-to-buffer (get-buffer ledger-report-buffer-name))
  345. (shrink-window-if-larger-than-buffer)
  346. (setq buffer-read-only nil)
  347. (setq ledger-report-cursor-line-number (line-number-at-pos))
  348. (erase-buffer)
  349. (ledger-do-report ledger-report-cmd)
  350. (setq buffer-read-only nil)
  351. (if ledger-report-is-reversed (ledger-report-reverse-lines))
  352. (if ledger-report-auto-refresh-sticky-cursor (forward-line (- ledger-report-cursor-line-number 5)))
  353. (pop-to-buffer cur-buf)))))
  354. (defun ledger-report-quit ()
  355. "Quit the ledger report buffer."
  356. (interactive)
  357. (ledger-report-goto)
  358. (set-window-configuration ledger-original-window-cfg)
  359. (kill-buffer (get-buffer ledger-report-buffer-name)))
  360. (defun ledger-report-edit-reports ()
  361. "Edit the defined ledger reports."
  362. (interactive)
  363. (customize-variable 'ledger-reports))
  364. (defun ledger-report-edit-report ()
  365. (interactive)
  366. "Edit the current report command in the mini buffer and re-run the report"
  367. (setq ledger-report-cmd (ledger-report-read-command ledger-report-cmd))
  368. (ledger-report-redo))
  369. (defun ledger-report-read-new-name ()
  370. "Read the name for a new report from the minibuffer."
  371. (let ((name ""))
  372. (while (ledger-report-string-empty-p name)
  373. (setq name (read-from-minibuffer "Report name: " nil nil nil
  374. 'ledger-report-name-prompt-history)))
  375. name))
  376. (defun ledger-report-save ()
  377. "Save the current report command line as a named report."
  378. (interactive)
  379. (ledger-report-goto)
  380. (let (existing-name)
  381. (when (ledger-report-string-empty-p ledger-report-name)
  382. (setq ledger-report-name (ledger-report-read-new-name)))
  383. (if (setq existing-name (ledger-report-name-exists ledger-report-name))
  384. (cond ((y-or-n-p (format "Overwrite existing report named '%s'? "
  385. ledger-report-name))
  386. (if (string-equal
  387. ledger-report-cmd
  388. (car (cdr (assq existing-name ledger-reports))))
  389. (message "Nothing to save. Current command is identical to existing saved one")
  390. (progn
  391. (setq ledger-reports
  392. (assq-delete-all existing-name ledger-reports))
  393. (ledger-reports-add ledger-report-name ledger-report-cmd)
  394. (ledger-reports-custom-save))))
  395. (t
  396. (progn
  397. (setq ledger-report-name (ledger-report-read-new-name))
  398. (ledger-reports-add ledger-report-name ledger-report-cmd)
  399. (ledger-reports-custom-save)))))))
  400. (provide 'ledger-report)
  401. ;;; ledger-report.el ends here