config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

ledger-report.el (27450B)


      1 ;;; ledger-report.el --- Helper code for use with the "ledger" command-line tool  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2003-2016 John Wiegley (johnw AT gnu DOT org)
      4 
      5 ;; This file is not part of GNU Emacs.
      6 
      7 ;; This is free software; you can redistribute it and/or modify it under
      8 ;; the terms of the GNU General Public License as published by the Free
      9 ;; Software Foundation; either version 2, or (at your option) any later
     10 ;; version.
     11 ;;
     12 ;; This is distributed in the hope that it will be useful, but WITHOUT
     13 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     14 ;; FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     15 ;; for more details.
     16 ;;
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
     19 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
     20 ;; MA 02110-1301 USA.
     21 
     22 
     23 ;;; Commentary:
     24 ;;  Provide facilities for running and saving reports in Emacs
     25 
     26 ;;; Code:
     27 
     28 (require 'ledger-xact)
     29 (require 'ledger-navigate)
     30 (require 'ledger-commodities)
     31 (require 'ledger-complete)
     32 (declare-function ledger-read-string-with-default "ledger-mode" (prompt default))
     33 (declare-function ledger-read-account-with-prompt "ledger-mode" (prompt))
     34 (declare-function ledger-read-payee-with-prompt "ledger-mode" (prompt))
     35 
     36 (require 'easymenu)
     37 (require 'ansi-color)
     38 (require 'font-lock)
     39 (eval-when-compile
     40   (require 'rx)
     41   (require 'subr-x))
     42 
     43 (defgroup ledger-report nil
     44   "Customization option for the Report buffer."
     45   :group 'ledger)
     46 
     47 (defcustom ledger-reports
     48   '(("bal" "%(binary) -f %(ledger-file) bal")
     49     ("reg" "%(binary) -f %(ledger-file) reg")
     50     ("payee" "%(binary) -f %(ledger-file) reg @%(payee)")
     51     ("account" "%(binary) -f %(ledger-file) reg %(account)"))
     52   "Definition of reports to run.
     53 
     54 Each element has the form (NAME CMDLINE).  The command line can
     55 contain format specifiers that are replaced with context sensitive
     56 information.  Format specifiers have the format '%(<name>)' where
     57 <name> is an identifier for the information to be replaced.  The
     58 `ledger-report-format-specifiers' alist variable contains a mapping
     59 from format specifier identifier to a Lisp function that implements
     60 the substitution.  See the documentation of the individual functions
     61 in that variable for more information on the behavior of each
     62 specifier."
     63   :type '(repeat (list (string :tag "Report Name")
     64                        (string :tag "Command Line")))
     65   :group 'ledger-report)
     66 
     67 (defcustom ledger-report-format-specifiers
     68   '(("ledger-file" . ledger-report-ledger-file-format-specifier)
     69     ("binary" . ledger-report-binary-format-specifier)
     70     ("payee" . ledger-report-payee-format-specifier)
     71     ("account" . ledger-report-account-format-specifier)
     72     ("month" . ledger-report-month-format-specifier)
     73     ("tagname" . ledger-report-tagname-format-specifier)
     74     ("tagvalue" . ledger-report-tagvalue-format-specifier))
     75   "An alist mapping ledger report format specifiers to implementing functions.
     76 
     77 The function is called with no parameters and expected to return
     78 a string, or a list of strings, that should replace the format specifier.
     79 Single strings are quoted with `shell-quote-argument'; lists of strings are
     80 simply concatenated (no quoting)."
     81   :type '(alist :key-type string
     82                 :value-type function)
     83   :group 'ledger-report)
     84 
     85 (defcustom ledger-report-auto-refresh t
     86   "If non-nil, automatically rerun the report when the ledger buffer is saved."
     87   :type 'boolean
     88   :group 'ledger-report)
     89 
     90 (defcustom ledger-report-auto-refresh-sticky-cursor nil
     91   "If non-nil, keep cursor's relative position after auto-refresh."
     92   :type 'boolean
     93   :group 'ledger-report)
     94 
     95 (defcustom ledger-report-links-in-register t
     96   "If non-nil, link entries in \"register\" reports to entries in the ledger buffer."
     97   :type 'boolean
     98   :group 'ledger-report)
     99 
    100 (defcustom ledger-report-links-beginning-of-xact t
    101   "If nil, links in \"register\" reports visit the posting they correspond to.
    102 
    103 If non-nil, visit the beginning of the transaction instead."
    104   :type 'boolean
    105   :group 'ledger-report)
    106 
    107 (defcustom ledger-report-use-native-highlighting t
    108   "When non-nil, use ledger's native highlighting in reports."
    109   :type 'boolean
    110   :package-version '(ledger-mode . "4.0.0")
    111   :group 'ledger-report)
    112 
    113 (defcustom ledger-report-native-highlighting-arguments '("--color" "--force-color")
    114   "List of ledger args needed by `ledger-report-use-native-highlighting'.
    115 
    116 If you are using hledger instead of ledger, you might want to set
    117 this variable to `(\"--color=always\")'."
    118   :type '(repeat string)
    119   :group 'ledger-report)
    120 
    121 (defcustom ledger-report-auto-width t
    122   "When non-nil, tell ledger about the width of the report window."
    123   :type 'boolean
    124   :package-version '(ledger-mode . "4.0.0")
    125   :group 'ledger-report)
    126 
    127 (defcustom ledger-report-use-header-line nil
    128   "If non-nil, indicate report name/command in the `header-line'.
    129 The report name/command won't be printed in the buffer.  See
    130 `ledger-report-header-line-fn' for how to customize the
    131 information reported."
    132   :type 'boolean
    133   :package-version '(ledger-mode . "4.0.0")
    134   :group 'ledger-report)
    135 
    136 (defcustom ledger-report-header-line-fn #'ledger-report--header-function
    137   "Evaluate this function in the `header-line' of the report buffer.
    138 `ledger-report-use-header-line' must be non-nil for this to have any effect."
    139   :type 'function
    140   :package-version '(ledger-mode . "4.0.0")
    141   :group 'ledger-report)
    142 
    143 (defcustom ledger-report-resize-window t
    144   "If non-nil, resize the report window.
    145 Calls `shrink-window-if-larger-than-buffer'."
    146   :type 'boolean
    147   :package-version '(ledger-mode . "4.0.0")
    148   :group 'ledger-report)
    149 
    150 (defcustom ledger-report-use-strict nil
    151   "When non-nil, `ledger-mode' will use --strict when running reports?"
    152   :type 'boolean
    153   :package-version '(ledger-mode . "4.0.0")
    154   :group 'ledger-report)
    155 
    156 (defcustom ledger-report-after-report-hook nil
    157   "Hook run after `ledger-report' has created the buffer and report."
    158   :type 'boolean
    159   :package-version '(ledger-mode . "4.0.0")
    160   :group 'ledger-report)
    161 
    162 (defvar ledger-report-buffer-name "*Ledger Report*")
    163 
    164 (defvar-local ledger-report-name nil)
    165 (defvar-local ledger-report-cmd nil)
    166 (defvar-local ledger-report-saved nil)
    167 (defvar-local ledger-report-current-month nil)
    168 (defvar-local ledger-report-is-reversed nil)
    169 (defvar-local ledger-report-cursor-line-number nil)
    170 (defvar-local ledger-report-ledger-buf nil)
    171 (defvar-local ledger-master-file nil
    172   "The master file for the current buffer.
    173 See documentation for the function `ledger-master-file'")
    174 
    175 (defvar ledger-report-name-prompt-history nil)
    176 (defvar ledger-report-cmd-prompt-history nil)
    177 (defvar ledger-minibuffer-history nil)
    178 (defvar ledger-report-mode-abbrev-table)
    179 
    180 (defun ledger-report-reverse-report ()
    181   "Reverse the order of the report."
    182   (interactive)
    183   (ledger-report-reverse-lines)
    184   (setq ledger-report-is-reversed (not ledger-report-is-reversed)))
    185 
    186 (defun ledger-report-reverse-lines ()
    187   "Reverse the lines in the ledger report buffer."
    188   (with-silent-modifications
    189     (goto-char (point-min))
    190     (unless ledger-report-use-header-line
    191       (forward-paragraph)
    192       (forward-line))
    193     (save-excursion
    194       (reverse-region (point) (point-max)))))
    195 
    196 (defun ledger-report-maybe-shrink-window ()
    197   "Shrink window if `ledger-report-resize-window' is non-nil."
    198   (when ledger-report-resize-window
    199     (shrink-window-if-larger-than-buffer)))
    200 
    201 (defvar ledger-report-mode-map
    202   (let ((map (make-sparse-keymap)))
    203     (define-key map (kbd "r") #'ledger-report-redo)
    204     (define-key map (kbd "R") #'ledger-report-reverse-report)
    205     (define-key map (kbd "s") #'ledger-report-save)
    206     (define-key map (kbd "S") #'ledger-report)
    207     (define-key map (kbd "e") #'ledger-report-edit-report)
    208     (define-key map (kbd "E") #'ledger-report-edit-reports)
    209     (define-key map (kbd "q") #'ledger-report-quit)
    210     (define-key map (kbd "C-c C-l C-r") #'ledger-report-redo)
    211     (define-key map (kbd "C-c C-l C-S-s") #'ledger-report-save)
    212     (define-key map (kbd "C-c C-l C-e") #'ledger-report-edit-report)
    213     (define-key map (kbd "C-c C-o C-r") #'ledger-report)
    214     (define-key map (kbd "M-p") #'ledger-report-previous-month)
    215     (define-key map (kbd "M-n") #'ledger-report-next-month)
    216     (define-key map (kbd "$") #'ledger-report-toggle-default-commodity)
    217     map)
    218   "Keymap for `ledger-report-mode'.")
    219 
    220 (easy-menu-define ledger-report-mode-menu ledger-report-mode-map
    221   "Ledger report menu."
    222   '("Reports"
    223     ["Select Report" ledger-report]
    224     ["Save Report" ledger-report-save]
    225     ["Edit Current Report" ledger-report-edit-report]
    226     ["Edit All Reports" ledger-report-edit-reports]
    227     ["Re-run Report" ledger-report-redo]
    228     "---"
    229     ["Reverse report order" ledger-report-reverse-report]
    230     "---"
    231     ["Scroll Up" scroll-up]
    232     ["Visit Source" ledger-report-visit-source]
    233     ["Scroll Down" scroll-down]
    234     "---"
    235     ["Quit" ledger-report-quit]
    236     ))
    237 
    238 (define-derived-mode ledger-report-mode special-mode "Ledger-Report"
    239   "A mode for viewing ledger reports."
    240   (setq-local revert-buffer-function #'ledger-report-redo)
    241   (hack-dir-local-variables-non-file-buffer))
    242 
    243 (defconst ledger-report--extra-args-marker "[[ledger-mode-flags]]")
    244 
    245 (defun ledger-report-binary-format-specifier ()
    246   "Return the path to ledger, plus a marker for extra arguments."
    247   (list (shell-quote-argument ledger-binary-path)
    248         ledger-report--extra-args-marker))
    249 
    250 (defun ledger-report-tagname-format-specifier ()
    251   "Return a valid meta-data tag name."
    252   ;; It is intended completion should be available on existing tag
    253   ;; names, but it remains to be implemented.
    254   (ledger-read-string-with-default "Tag Name" nil))
    255 
    256 (defun ledger-report-tagvalue-format-specifier ()
    257   "Return a valid meta-data tag name."
    258   ;; It is intended completion should be available on existing tag
    259   ;; values, but it remains to be implemented.
    260   (ledger-read-string-with-default "Tag Value" nil))
    261 
    262 (defun ledger-report-read-name ()
    263   "Read the name of a ledger report to use, with completion.
    264 
    265 The empty string and unknown names are allowed."
    266   (completing-read "Report name: "
    267                    ledger-reports nil nil nil
    268                    'ledger-report-name-prompt-history nil))
    269 
    270 (defun ledger-report (report-name edit)
    271   "Run a user-specified report from `ledger-reports'.
    272 
    273 Prompts the user for the REPORT-NAME of the report to run or
    274 EDIT.  If no name is entered, the user will be prompted for a
    275 command line to run.  The command line specified or associated
    276 with the selected report name is run and the output is made
    277 available in another buffer for viewing.  If a prefix argument is
    278 given and the user selects a valid report name, the user is
    279 prompted with the corresponding command line for editing before
    280 the command is run.
    281 
    282 The output buffer will be in `ledger-report-mode', which defines
    283 commands for saving a new named report based on the command line
    284 used to generate the buffer, navigating the buffer, etc."
    285   (interactive
    286    (progn
    287      (when (and (buffer-modified-p)
    288                 (y-or-n-p "Buffer modified, save it? "))
    289        (save-buffer))
    290      (let ((rname (ledger-report-read-name))
    291            (edit (not (null current-prefix-arg))))
    292        (list rname edit))))
    293   (let* ((file (ledger-master-file))
    294          (buf (find-file-noselect file)))
    295     (with-current-buffer
    296         (pop-to-buffer (get-buffer-create ledger-report-buffer-name))
    297       (ledger-report-mode)
    298       (setq ledger-report-saved nil)
    299       (setq ledger-report-ledger-buf buf)
    300       (setq ledger-report-name report-name)
    301       (setq ledger-report-is-reversed nil)
    302       (setq ledger-report-current-month nil)
    303       (setq ledger-master-file file)
    304       (ledger-report-cmd report-name edit)
    305       (with-silent-modifications
    306         (erase-buffer)
    307         (ledger-do-report ledger-report-cmd))
    308       (ledger-report-maybe-shrink-window)
    309       (run-hooks 'ledger-report-after-report-hook)
    310       (message (substitute-command-keys (concat "\\[ledger-report-quit] to quit; "
    311                                                 "\\[ledger-report-redo] to redo; "
    312                                                 "\\[ledger-report-edit-report] to edit; "
    313                                                 "\\[ledger-report-save] to save; "
    314                                                 "\\[scroll-up-command] and \\[scroll-down-command] to scroll"))))))
    315 
    316 (defun ledger-report--header-function ()
    317   "Compute the string to be used as the header in the `ledger-report' buffer."
    318   (format "Ledger Report: %s -- Buffer: %s -- Command: %s"
    319           (propertize ledger-report-name 'face 'font-lock-constant-face)
    320           (propertize (buffer-name ledger-report-ledger-buf) 'face 'font-lock-string-face)
    321           (propertize ledger-report-cmd 'face 'font-lock-comment-face)))
    322 
    323 (defun ledger-report-name-exists (name)
    324   "Check to see if the given report NAME exists.
    325 
    326 If exists, returns the object naming the report, otherwise
    327 returns nil."
    328   (unless (string-empty-p name)
    329     (car (assoc name ledger-reports))))
    330 
    331 (defun ledger-reports-add (name cmd)
    332   "Add a new report NAME and CMD to `ledger-reports'."
    333   (setq ledger-reports (cons (list name cmd) ledger-reports)))
    334 
    335 (defun ledger-reports-custom-save ()
    336   "Save the `ledger-reports' variable using the customize framework."
    337   (customize-save-variable 'ledger-reports ledger-reports))
    338 
    339 (defun ledger-report-read-command (report-cmd)
    340   "Read the command line to create a report from REPORT-CMD."
    341   (read-from-minibuffer "Report command line: "
    342                         (if (null report-cmd) "ledger " report-cmd)
    343                         nil nil 'ledger-report-cmd-prompt-history))
    344 
    345 (defun ledger-report-ledger-file-format-specifier ()
    346   "Substitute the full path to master or current ledger file.
    347 
    348 The master file name is determined by the function
    349 `ledger-master-file', which depends on the variable of the same
    350 name.  If it is non-nil, it is used, otherwise the current
    351 buffer's file is used."
    352   (ledger-master-file))
    353 
    354 ;; General helper functions
    355 
    356 (defun ledger-master-file ()
    357   "Return the master file for a ledger file.
    358 
    359 The master file is either the file for the current ledger buffer
    360 or the file specified by the buffer-local variable
    361 `ledger-master-file'.  Typically this variable would be set in a
    362 file local variable comment block at the end of a ledger file
    363 which is included in some other file."
    364   (if ledger-master-file
    365       (expand-file-name ledger-master-file)
    366     (buffer-file-name)))
    367 
    368 (defun ledger-report-payee-format-specifier ()
    369   "Substitute a payee name.
    370 
    371 The user is prompted to enter a payee and that is substituted.
    372 If point is in an xact, the payee for that xact is used as the
    373 default."
    374   (ledger-read-payee-with-prompt "Payee"))
    375 
    376 (defun ledger-report-account-format-specifier ()
    377   "Substitute an account name.
    378 
    379 The user is prompted to enter an account name, which can be any
    380 regular expression identifying an account.  If point is on an
    381 account posting line for an xact, the full account name on that
    382 line is the default."
    383   (ledger-read-account-with-prompt "Account"))
    384 
    385 (defun ledger-report--current-month ()
    386   "Return current month as (YEAR . MONTH-INDEX).
    387 
    388 MONTH-INDEX ranges from 1 (January) to 12 (December) and YEAR is
    389 a number."
    390   (let* ((time-parts (decode-time))
    391          (year (nth 5 time-parts))
    392          (month-index (nth 4 time-parts)))
    393     (cons year month-index)))
    394 
    395 (defun ledger-report--normalize-month (month)
    396   "Return (YEAR . NEW-MONTH) where NEW-MONTH is between 1 and 12.
    397 
    398 MONTH is of the form (YEAR . INDEX) where INDEX is an integer.
    399 The purpose of this method is then to convert any year/month pair
    400 to a meaningful date, e.g., from (2018 . -2) to (2017 . 10)."
    401   (let* ((month-index (cdr month))
    402          (year-shift  (/ (1- month-index) 12)))
    403     (when (<= month-index 0)
    404       (setq year-shift (1- year-shift)))
    405     (cons (+ (car month) year-shift)
    406           (1+ (mod (1- month-index) 12)))))
    407 
    408 (defun ledger-report--shift-month (month shift)
    409   "Return (YEAR . NEW-MONTH) where NEW-MONTH is MONTH+SHIFT.
    410 
    411 MONTH is of the form (YEAR . INDEX) where INDEX ranges from
    412 1 (January) to 12 (December) and YEAR is a number."
    413   (let* ((year (car month))
    414          (new-month (+ (cdr month) shift)))
    415     (ledger-report--normalize-month (cons year new-month))))
    416 
    417 (defun ledger-report-month-format-specifier ()
    418   "Substitute current month."
    419   (with-current-buffer (or ledger-report-buffer-name (current-buffer))
    420     (let* ((month (or ledger-report-current-month (ledger-report--current-month)))
    421            (year (car month))
    422            (month-index (cdr month)))
    423       (format "%s-%s" year month-index))))
    424 
    425 (defun ledger-report-expand-format-specifiers (report-cmd)
    426   "Expand format specifiers in REPORT-CMD.
    427 
    428 Format specifiers are defined in the
    429 `ledger-report-format-specifiers' alist.  The functions are
    430 called in the ledger buffer for which the report is being run."
    431   (let ((ledger-buf ledger-report-ledger-buf))
    432     (with-temp-buffer
    433       (save-excursion (insert report-cmd))
    434       (while (re-search-forward "%(\\([^)]*\\))" nil t)
    435         (when-let ((specifier (match-string 1))
    436                    (f (cdr (assoc specifier ledger-report-format-specifiers))))
    437           (let* ((arg (save-match-data
    438                         (with-current-buffer ledger-buf
    439                           (funcall f))))
    440                  (quoted (save-match-data
    441                            (if (listp arg)
    442                                (string-join arg " ")
    443                              (shell-quote-argument arg)))))
    444             (replace-match quoted 'fixedcase 'literal))))
    445        (buffer-string))))
    446 
    447 (defun ledger-report--cmd-needs-links-p (cmd)
    448   "Check links should be added to the report produced by CMD."
    449   ;; --subtotal reports do not produce identifiable transactions, so
    450   ;; don't prepend location information for them
    451   (and (string-match "\\<reg\\(ister\\)?\\>" cmd)
    452        ledger-report-links-in-register
    453        (not (string-match "--subtotal" cmd))))
    454 
    455 (defun ledger-report--compute-extra-args (report-cmd)
    456   "Compute extra args to add to REPORT-CMD."
    457   `(,@(when (ledger-report--cmd-needs-links-p report-cmd)
    458         '("--prepend-format=%(filename):%(beg_line):"))
    459     ,@(when ledger-report-auto-width
    460         `("--columns" ,(format "%d" (window-max-chars-per-line))))
    461     ,@(when ledger-report-use-native-highlighting
    462         ledger-report-native-highlighting-arguments)
    463     ,@(when ledger-report-use-strict
    464         '("--strict"))))
    465 
    466 (defun ledger-report-cmd (report-name edit)
    467   "Get the command line to run the report name REPORT-NAME.
    468 Optionally EDIT the command."
    469   (let ((report-cmd (car (cdr (assoc report-name ledger-reports)))))
    470     ;; logic for substitution goes here
    471     (when (or (null report-cmd) edit)
    472       (setq report-cmd (ledger-report-read-command report-cmd))
    473       (setq ledger-report-saved nil)) ;; this is a new report, or edited report
    474     (setq report-cmd (ledger-report-expand-format-specifiers report-cmd))
    475     (setq ledger-report-cmd report-cmd)
    476     (or (string-empty-p report-name)
    477         (ledger-report-name-exists report-name)
    478         (progn
    479           (ledger-reports-add report-name report-cmd)
    480           (ledger-reports-custom-save)))
    481     report-cmd))
    482 
    483 (define-button-type 'ledger-report-register-entry
    484   'follow-link t
    485   'face nil ;; Otherwise make-text-button replaces Ledger's native highlighting
    486   'action (lambda (_button) (ledger-report-visit-source)))
    487 
    488 (defun ledger-report--change-month (shift)
    489   "Rebuild report with transactions from current month + SHIFT."
    490   (let* ((current-month (or ledger-report-current-month (ledger-report--current-month)))
    491          (previous-month (ledger-report--shift-month current-month shift)))
    492     (setq ledger-report-current-month previous-month)
    493     (ledger-report-cmd ledger-report-name nil)
    494     (ledger-report-redo)))
    495 
    496 (defun ledger-report--add-links ()
    497   "Replace file and line annotations with buttons."
    498   (while (re-search-forward "^\\(\\(?:/\\|[a-zA-Z]:[\\/]\\)[^:]+\\)?:\\([0-9]+\\)?:" nil t)
    499     (let ((file (match-string 1))
    500           (line (string-to-number (match-string 2))))
    501       (delete-region (match-beginning 0) (match-end 0))
    502       (when (and file line)
    503         (add-text-properties (line-beginning-position) (line-end-position)
    504                              (list 'ledger-source (cons file line)))
    505         (make-text-button
    506          (line-beginning-position) (line-end-position)
    507          'type 'ledger-report-register-entry
    508          'help-echo (format "mouse-2, RET: Visit %s:%d" file line))
    509         ;; Appending the face preserves Ledger's native highlighting
    510         (font-lock-append-text-property (line-beginning-position) (line-end-position)
    511                                         'face 'ledger-font-report-clickable-face)
    512         (end-of-line)))))
    513 
    514 (defun ledger-report--compute-header-line (cmd)
    515   "Call `ledger-report-header-line-fn' with `ledger-report-cmd' bound to CMD."
    516   (let ((ledger-report-cmd cmd))
    517     (funcall ledger-report-header-line-fn)))
    518 
    519 (defun ledger-do-report (cmd)
    520   "Run a report command line CMD.
    521 CMD may contain a (shell-quoted) version of
    522 `ledger-report--extra-args-marker', which will be replaced by
    523 arguments returned by `ledger-report--compute-extra-args'."
    524   (goto-char (point-min))
    525   (let* ((marker ledger-report--extra-args-marker)
    526          (marker-re (concat " *" (regexp-quote marker)))
    527          (args (ledger-report--compute-extra-args cmd))
    528          (args-str (concat " " (mapconcat #'shell-quote-argument args " ")))
    529          (clean-cmd (replace-regexp-in-string marker-re "" cmd t t))
    530          (real-cmd (replace-regexp-in-string marker-re args-str cmd t t)))
    531     (setq header-line-format
    532           (and ledger-report-use-header-line
    533                `(:eval (ledger-report--compute-header-line ,clean-cmd))))
    534     (unless ledger-report-use-header-line
    535       (insert (format "Report: %s\n" ledger-report-name)
    536               (format "Command: %s\n" clean-cmd)
    537               (make-string (- (window-width) 1) ?=)
    538               "\n\n"))
    539     (let* ((report (shell-command-to-string real-cmd)))
    540       (when ledger-report-use-native-highlighting
    541         (setq report (ansi-color-apply report)))
    542       (save-excursion
    543         (insert report))
    544       (when (ledger-report--cmd-needs-links-p cmd)
    545         (save-excursion
    546           (ledger-report--add-links))))))
    547 
    548 (defun ledger-report-visit-source ()
    549   "Visit the transaction under point in the report window.
    550 
    551 If `ledger-report-links-beginning-of-xact' is nil, visit the
    552 specific posting at point instead."
    553   (interactive)
    554   (let* ((prop (get-text-property (point) 'ledger-source))
    555          (file (car prop))
    556          (line (cdr prop)))
    557     (when (and file line)
    558       (find-file-other-window file)
    559       (widen)
    560       (goto-char (point-min))
    561       (forward-line (1- line))
    562       (when ledger-report-links-beginning-of-xact
    563         (ledger-navigate-beginning-of-xact)))))
    564 
    565 (defun ledger-report-goto ()
    566   "Goto the ledger report buffer."
    567   (interactive)
    568   (let ((rbuf (get-buffer ledger-report-buffer-name)))
    569     (if (not rbuf)
    570         (error "There is no ledger report buffer"))
    571     (pop-to-buffer rbuf)
    572     (ledger-report-maybe-shrink-window)))
    573 
    574 (defun ledger-report-redo (&optional _ignore-auto _noconfirm)
    575   "Redo the report in the current ledger report buffer.
    576 IGNORE-AUTO and NOCONFIRM are for compatibility with
    577 `revert-buffer-function' and are currently ignored."
    578   (interactive)
    579   (unless (or (derived-mode-p 'ledger-mode)
    580               (derived-mode-p 'ledger-report-mode))
    581     (user-error "Not in a ledger-mode or ledger-report-mode buffer"))
    582   (let ((cur-buf (current-buffer)))
    583     (when (and ledger-report-auto-refresh
    584                (get-buffer ledger-report-buffer-name))
    585       (pop-to-buffer (get-buffer ledger-report-buffer-name))
    586       (ledger-report-maybe-shrink-window)
    587       (setq ledger-report-cursor-line-number (line-number-at-pos))
    588       (with-silent-modifications
    589         (erase-buffer)
    590         (ledger-do-report ledger-report-cmd)
    591         (when ledger-report-is-reversed
    592           (ledger-report-reverse-lines))
    593         (when ledger-report-auto-refresh-sticky-cursor
    594           (forward-line (- ledger-report-cursor-line-number 5))))
    595       (run-hooks 'ledger-report-after-report-hook)
    596       (pop-to-buffer cur-buf))))
    597 
    598 (defun ledger-report-quit ()
    599   "Quit the ledger report buffer and kill its buffer."
    600   (interactive)
    601   (unless (buffer-live-p (get-buffer ledger-report-buffer-name))
    602     (user-error "No ledger report buffer"))
    603   (quit-windows-on ledger-report-buffer-name 'kill))
    604 
    605 (define-obsolete-function-alias 'ledger-report-kill #'ledger-report-quit "2018-03-18")
    606 
    607 (defun ledger-report-edit-reports ()
    608   "Edit the defined ledger reports."
    609   (interactive)
    610   (customize-variable 'ledger-reports))
    611 
    612 (defun ledger-report-edit-report ()
    613   "Edit the current report command in the mini buffer and re-run the report."
    614   (interactive)
    615   (setq ledger-report-cmd (ledger-report-read-command ledger-report-cmd))
    616   (ledger-report-redo))
    617 
    618 (define-obsolete-function-alias 'ledger-report-select-report #'ledger-report "ledger 4.0.0")
    619 
    620 (defun ledger-report-read-new-name ()
    621   "Read the name for a new report from the minibuffer."
    622   (let ((name ""))
    623     (while (string-empty-p name)
    624       (setq name (read-from-minibuffer "Report name: " nil nil nil
    625                                        'ledger-report-name-prompt-history)))
    626     name))
    627 
    628 (defun ledger-report-save ()
    629   "Save the current report command line as a named report."
    630   (interactive)
    631   (ledger-report-goto)
    632   (when (string-empty-p ledger-report-name)
    633     (setq ledger-report-name (ledger-report-read-new-name)))
    634 
    635   (when-let ((existing-name (ledger-report-name-exists ledger-report-name)))
    636     (cond ((y-or-n-p (format "Overwrite existing report named '%s'? "
    637                              ledger-report-name))
    638            (if (string-equal
    639                 ledger-report-cmd
    640                 (car (cdr (assq existing-name ledger-reports))))
    641                (message "Nothing to save. Current command is identical to existing saved one")
    642              (setq ledger-reports
    643                    (assq-delete-all existing-name ledger-reports))
    644              (ledger-reports-add ledger-report-name ledger-report-cmd)
    645              (ledger-reports-custom-save)))
    646           (t
    647            (setq ledger-report-name (ledger-report-read-new-name))
    648            (ledger-reports-add ledger-report-name ledger-report-cmd)
    649            (ledger-reports-custom-save)))))
    650 
    651 (defun ledger-report-previous-month ()
    652   "Rebuild report with transactions from the previous month."
    653   (interactive)
    654   (ledger-report--change-month -1))
    655 
    656 (defun ledger-report-next-month ()
    657   "Rebuild report with transactions from the next month."
    658   (interactive)
    659   (ledger-report--change-month 1))
    660 
    661 (defun ledger-report-toggle-default-commodity ()
    662   "Toggle exchange of reported amounts to `ledger-reconcile-default-commodity'."
    663   (interactive)
    664   (unless (derived-mode-p 'ledger-report-mode)
    665     (user-error "Not a ledger report buffer"))
    666   (save-match-data
    667     (if (string-match
    668          (concat (rx (or "--exchange" "-X") (1+ space))
    669                  (regexp-quote ledger-reconcile-default-commodity))
    670          ledger-report-cmd)
    671         (setq ledger-report-cmd (replace-match "" nil nil ledger-report-cmd))
    672       (setq ledger-report-cmd (concat ledger-report-cmd
    673                                       " --exchange " ledger-reconcile-default-commodity))))
    674   (ledger-report-redo))
    675 
    676 (provide 'ledger-report)
    677 
    678 ;;; ledger-report.el ends here