Skip to content

Commit f953f98

Browse files
committed
Display live preview of ledger xact while inputting transaction text
1 parent 88cd097 commit f953f98

File tree

1 file changed

+113
-1
lines changed

1 file changed

+113
-1
lines changed

ledger-xact.el

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
(require 'ledger-exec)
3232
(require 'ledger-post)
3333
(declare-function ledger-read-date "ledger-mode" (prompt))
34+
(declare-function ledger-mode "ledger-mode" ())
3435

3536
;; TODO: This file depends on code in ledger-mode.el, which depends on this.
3637

@@ -46,6 +47,15 @@ When nil, `ledger-add-transaction' will not prompt twice."
4647
:package-version '(ledger-mode . "4.0.1")
4748
:group 'ledger)
4849

50+
(defcustom ledger-add-transaction-idle-preview t
51+
"When non-nil, a live preview of the to-be-added transaction is shown.
52+
Requires `ledger-add-transaction-prompt-for-text' to be non-nil."
53+
:type '(choice (const :tag "Do not preview" nil)
54+
(const :tag "Preview when idle" t)
55+
(number :tag "Preview with custom delay"))
56+
:package-version '(ledger-mode . "4.1")
57+
:group 'ledger)
58+
4959
(defvar-local ledger-xact-highlight-overlay (list))
5060

5161
(defun ledger-highlight-make-overlay ()
@@ -172,12 +182,111 @@ Leave point on the first amount, if any, otherwise the first account."
172182
(defvar ledger-add-transaction-last-date nil
173183
"Last date entered using `ledger-read-transaction'.")
174184

185+
(defvar ledger-xact--preview-buffer-name "*ledger xact preview*")
186+
(defvar-local ledger-xact--preview-timer nil)
187+
(defvar-local ledger-xact--date nil
188+
"In a minibuffer for the transaction text, the transaction date.")
189+
(defvar-local ledger-xact--ledger-buf nil
190+
"In a minibuffer for the transaction text, the original ledger buffer.")
191+
192+
(defun ledger-xact--create-preview-buffer ()
193+
"Create a buffer to display ledger xact output previews and return it.
194+
195+
If the buffer already exists, return it as-is."
196+
(or (get-buffer ledger-xact--preview-buffer-name)
197+
(with-current-buffer (get-buffer-create ledger-xact--preview-buffer-name)
198+
;; Enable `ledger-mode' just for syntax highlighting. Skip all minor
199+
;; modes except for `font-lock-mode'.
200+
(delay-mode-hooks (ledger-mode))
201+
(font-lock-mode)
202+
(setq buffer-read-only t)
203+
(set-buffer-modified-p nil)
204+
(current-buffer))))
205+
206+
(defun ledger-xact--preview (ledger-buf date args)
207+
"Run \"ledger xact\" with DATE and ARGS and display the output.
208+
209+
LEDGER-BUF's contents are passed as input to \"ledger xact\".
210+
211+
Return the window displaying the output buffer, or nil if it was not
212+
displayed."
213+
(let ((preview-buf (ledger-xact--create-preview-buffer)))
214+
(with-current-buffer preview-buf
215+
(with-silent-modifications
216+
;; Don't use `ledger-exec-ledger' because it pops up any error
217+
;; output in a separate buffer. For this use case, it is
218+
;; preferable to display the error in the preview buffer
219+
;; instead.
220+
(with-current-buffer ledger-buf
221+
(apply #'call-process-region
222+
nil nil ledger-binary-path
223+
nil preview-buf t
224+
"xact" date args))
225+
(ledger-post-align-postings (point-min) (point-max)))
226+
(goto-char (point-min)))
227+
(display-buffer preview-buf
228+
'((display-buffer-reuse-window display-buffer-at-bottom)
229+
(window-height . fit-window-to-buffer)))))
230+
231+
(defun ledger-xact--preview-timer (minibuffer)
232+
"Preview the ledger xact output from MINIBUFFER's current contents."
233+
(setq ledger-xact--preview-timer nil)
234+
;; TODO: It would be more correct to use `minibufferp' and pass a non-nil LIVE
235+
;; argument, but that feature isn't available until Emacs 28.3.
236+
(when (and (buffer-live-p minibuffer)
237+
(eq minibuffer (window-buffer (active-minibuffer-window))))
238+
(with-current-buffer minibuffer
239+
(let ((ledger-buf ledger-xact--ledger-buf)
240+
(date ledger-xact--date))
241+
(when-let* ((args (ledger-parse-transaction-text (minibuffer-contents))))
242+
(while-no-input
243+
(let ((window
244+
(ledger-xact--preview ledger-buf date args)))
245+
(setq-local minibuffer-scroll-window window))))))))
246+
247+
(defun ledger-xact--after-change-function (_beg _end _len)
248+
"Added to `after-change-functions' in transaction-reading minibuffers."
249+
(when-let* ((buf (get-buffer ledger-xact--preview-buffer-name)))
250+
(with-current-buffer buf
251+
(with-silent-modifications
252+
(erase-buffer))))
253+
(unless ledger-xact--preview-timer
254+
(setq ledger-xact--preview-timer
255+
(run-with-idle-timer
256+
(if (numberp ledger-add-transaction-idle-preview)
257+
ledger-add-transaction-idle-preview
258+
0.1)
259+
nil #'ledger-xact--preview-timer (current-buffer)))))
260+
261+
(defun ledger-xact--hide-preview-window ()
262+
"Similar to `minibuffer-restore-windows', for transaction-reading minibuffers."
263+
;; This variable was introduced in Emacs 28.1. The default, matching the
264+
;; behavior in previous versions of Emacs, is equivalent to non-nil. We only
265+
;; want to delete the window if the default window configuration restore logic
266+
;; wouldn't have.
267+
(when (and (boundp 'read-minibuffer-restore-windows)
268+
(not read-minibuffer-restore-windows))
269+
(when-let* ((window (get-buffer-window ledger-xact--preview-buffer-name)))
270+
(delete-window window))))
271+
272+
(defun ledger-read-transaction-text (date)
273+
"Read the text of a transaction with date DATE."
274+
(let ((ledger-buf (current-buffer)))
275+
(minibuffer-with-setup-hook
276+
(lambda ()
277+
(setq ledger-xact--date date
278+
ledger-xact--ledger-buf ledger-buf)
279+
(when ledger-add-transaction-idle-preview
280+
(add-hook 'after-change-functions #'ledger-xact--after-change-function nil t)
281+
(add-hook 'minibuffer-exit-hook #'ledger-xact--hide-preview-window nil t)))
282+
(read-string (concat "xact " date ": ") nil 'ledger-minibuffer-history))))
283+
175284
(defun ledger-read-transaction ()
176285
"Read the text of a transaction, which is at least the current date."
177286
(let ((date (ledger-read-date "Date: ")))
178287
(concat date " "
179288
(when ledger-add-transaction-prompt-for-text
180-
(read-string (concat "xact " date ": ") nil 'ledger-minibuffer-history)))))
289+
(ledger-read-transaction-text date)))))
181290

182291
(defun ledger-parse-iso-date (date)
183292
"Try to parse DATE using `ledger-iso-date-regexp' and return a time value or nil."
@@ -190,6 +299,9 @@ Leave point on the first amount, if any, otherwise the first account."
190299
"Parse TRANSACTION-TEXT as a date and maybe some arguments.
191300
192301
Return (DATE . ARGS), a list of strings."
302+
;; TODO: This whole function could just be replaced with
303+
;; `split-string-shell-command' when the minimum supported Emacs version is
304+
;; Emacs 28.
193305
(with-temp-buffer
194306
(insert transaction-text)
195307
(mapcar #'eval (eshell-parse-arguments (point-min) (point-max)))))

0 commit comments

Comments
 (0)