aboutsummaryrefslogtreecommitdiff
path: root/org-drill.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-21 07:27:58 -0500
committerCraig Jennings <c@cjennings.net>2026-07-21 07:27:58 -0500
commita30074db7a7ca06e1baf312928076130be37e676 (patch)
treef4fa4dd6ca6e1429fd88769edf66d47074379093 /org-drill.el
parent24f8c5aac8873837824ca6d61e566e3e24c96ad5 (diff)
downloadorg-drill-a30074db7a7ca06e1baf312928076130be37e676.tar.gz
org-drill-a30074db7a7ca06e1baf312928076130be37e676.zip
fix: scan the deck buffer, not the dashboard, in the statistics viewsHEADmain
With the default file scope, the dashboard rendered and exported from its own buffer, so every count read zero and cards.csv came out empty. The render, refresh, cycle, and CSV-export paths now run their card scans in the deck buffer the command was invoked from, stored buffer-locally, and a dead deck signals a clear user-error instead of scanning the wrong buffer. Card links are now a registered org-drill-card link type with a follow handler, and the link path carries the file identity so RET jumps work across files. Eleven new ERT tests cover the render, refresh, export, link, and dead-buffer paths.
Diffstat (limited to 'org-drill.el')
-rw-r--r--org-drill.el127
1 files changed, 93 insertions, 34 deletions
diff --git a/org-drill.el b/org-drill.el
index 11f161e..8c895b6 100644
--- a/org-drill.el
+++ b/org-drill.el
@@ -5003,9 +5003,11 @@ an integer (0 when absent). AVG-QUALITY is DRILL_AVERAGE_QUALITY as a
float, or nil when absent. DAYS-SINCE-REVIEW is the integer day count
since DRILL_LAST_REVIEWED, or nil when never reviewed. DAYS-SINCE-ADDED
is the integer day count since DATE_ADDED, or nil when absent.
-TOTAL-REPEATS is DRILL_TOTAL_REPEATS as an integer (0 when absent)."
+TOTAL-REPEATS is DRILL_TOTAL_REPEATS as an integer (0 when absent).
+FILE is the file the entry's buffer visits, or nil for a fileless
+buffer; card links carry it so the dashboard can jump across files."
heading pos failure-count avg-quality
- days-since-review days-since-added total-repeats)
+ days-since-review days-since-added total-repeats file)
;; Short aliases for the long struct accessors, used by the sort
;; comparators below so the comparator lines stay readable.
@@ -5057,7 +5059,8 @@ via `org-get-heading', so the point must be on a drill heading."
(org-drill-statistics--days-since-org-timestamp last-raw today-day)
:days-since-added
(org-drill-statistics--days-since-org-timestamp added-raw today-day)
- :total-repeats (if repeats-raw (string-to-number repeats-raw) 0))))
+ :total-repeats (if repeats-raw (string-to-number repeats-raw) 0)
+ :file (buffer-file-name))))
(defun org-drill-statistics--days-since-org-timestamp (timestamp today-day)
"Return integer days from org TIMESTAMP string to TODAY-DAY.
@@ -5266,25 +5269,51 @@ with no SCHEDULED time are ignored."
;; each scan and cap) gives the pre-cap totals needed for the "+N more"
;; footers for free.
-(defun org-drill-statistics--card-link (heading pos)
+(defun org-drill-statistics--card-link (heading pos &optional file)
"Return an org bracket link to a drill card heading.
HEADING is the card's outline heading string, used as the link
-description. POS is the integer buffer position of the heading, carried
-in the link path so the dashboard's RET handler can jump to the card.
-The path has the form \"org-drill-card:POS\". Any closing bracket in
-HEADING is replaced so a literal \"]]\" cannot terminate the link early.
-An empty or nil HEADING falls back to a position-based description."
+description. POS is the integer buffer position of the heading. FILE
+is the file the card's buffer visits, or nil for a fileless buffer.
+The path has the form \"org-drill-card:POS@FILE\" (FILE may be empty),
+so the follow handler can jump to the card even across files. Any
+closing bracket in HEADING is replaced so a literal \"]]\" cannot
+terminate the link early. An empty or nil HEADING falls back to a
+position-based description."
(let* ((desc (if (and heading (not (string-empty-p heading)))
heading
(format "card at %d" pos)))
(safe (replace-regexp-in-string "]" "}" desc)))
- (format "[[org-drill-card:%d][%s]]" pos safe)))
+ (format "[[org-drill-card:%d@%s][%s]]" pos (or file "") safe)))
+
+(defun org-drill-statistics--follow-card-link (path &optional _prefix)
+ "Follow an org-drill-card link to its card.
+PATH is \"POS@FILE\" (FILE may be empty) or a legacy bare \"POS\".
+With a file, visit it in another window and move point to POS. Without
+one, fall back to the dashboard's stored source buffer. Signals a
+`user-error' when neither identifies a live target."
+ (let* ((at (string-match "@" path))
+ (pos (string-to-number (if at (substring path 0 at) path)))
+ (file (and at (substring path (1+ at))))
+ (source (and (boundp 'org-drill-statistics--source-buffer)
+ org-drill-statistics--source-buffer)))
+ (cond
+ ((and file (not (string-empty-p file)))
+ (find-file-other-window file)
+ (goto-char (min (max 1 pos) (point-max))))
+ ((buffer-live-p source)
+ (pop-to-buffer source)
+ (goto-char (min (max 1 pos) (point-max))))
+ (t (user-error
+ "Card buffer is gone; refresh the dashboard from a deck buffer")))))
+
+(org-link-set-parameters "org-drill-card"
+ :follow #'org-drill-statistics--follow-card-link)
(defun org-drill-statistics--render-attention-table
(title rows total empty-note)
"Render one needs-attention subsection as an org string.
TITLE is the third-level heading text for the subsection. ROWS is a
-list of (HEADING . POS) cons cells, already capped at
+list of (HEADING POS FILE) lists, already capped at
`org-drill-statistics-attention-row-limit' and already sorted. TOTAL is
the full count of matching cards before capping, used for the
\"+N more\" footer. EMPTY-NOTE is the line shown when ROWS is empty.
@@ -5304,7 +5333,8 @@ the number of rows shown."
(mapconcat
(lambda (row)
(format "| %s |"
- (org-drill-statistics--card-link (car row) (cdr row))))
+ (org-drill-statistics--card-link
+ (nth 0 row) (nth 1 row) (nth 2 row))))
rows
"\n")
"\n"
@@ -5368,8 +5398,9 @@ pre-cap totals are available for the footers without rescanning."
(lambda (structs)
(mapcar
(lambda (d)
- (cons (org-drill-statistics--entry-attention-data-heading d)
- (org-drill-statistics--entry-attention-data-pos d)))
+ (list (org-drill-statistics--entry-attention-data-heading d)
+ (org-drill-statistics--entry-attention-data-pos d)
+ (org-drill-statistics--entry-attention-data-file d)))
(org-drill-statistics--cap-rows structs)))))
(concat
"** Needs attention\n"
@@ -5634,6 +5665,14 @@ algorithms. Set by `org-drill-statistics' and rotated by
`org-drill-statistics-cycle-algorithm'. Read by the render helpers via
the filtered log they are handed.")
+(defvar-local org-drill-statistics--source-buffer nil
+ "Buffer-local deck buffer the dashboard's card scans run in.
+Set by `org-drill-statistics' to the buffer the command was invoked
+from and read by `org-drill-statistics--render' (via refresh and the
+cycle commands) so every card traversal sees the deck, not the
+dashboard's own text. Also the fallback jump target for card links
+whose path carries no file.")
+
(defun org-drill-statistics--range-cutoff-float (label)
"Return the float-time cutoff for range LABEL, or nil for all time.
LABEL is a key in `org-drill-statistics-range-presets'. When its DAYS
@@ -5720,24 +5759,34 @@ is put in `org-mode'; not intended to be toggled by hand."
:lighter " OrgDrillStats"
:keymap org-drill-statistics-mode-map)
-(defun org-drill-statistics--render (buffer scope range algorithm)
+(defun org-drill-statistics--render (buffer scope range algorithm
+ &optional source)
"Render the dashboard for SCOPE, RANGE, ALGORITHM into BUFFER.
-BUFFER is the target buffer. Its read-only state is lifted for the
-write, the assembled body replaces the contents, point returns to the
-top, and the buffer is left read-only. The three filter values are
-stored buffer-locally so refresh and the cycle commands can read and
-rotate them. Returns BUFFER."
- (with-current-buffer buffer
- (let ((inhibit-read-only t)
- (body (org-drill-statistics--render-all scope range algorithm)))
- (erase-buffer)
- (insert body)
- (goto-char (point-min)))
- (setq org-drill-statistics--scope scope
- org-drill-statistics--range range
- org-drill-statistics--algorithm algorithm)
- (setq buffer-read-only t)
- buffer))
+BUFFER is the target buffer. SOURCE is the deck buffer the card scans
+run in, defaulting to the current buffer, so the traversals see the
+deck rather than the dashboard's own text. A dead SOURCE signals a
+`user-error'. BUFFER's read-only state is lifted for the write, the
+assembled body replaces the contents, point returns to the top, and the
+buffer is left read-only. The filter values and SOURCE are stored
+buffer-locally so refresh and the cycle commands can read and rotate
+them. Returns BUFFER."
+ (let ((source (or source (current-buffer))))
+ (unless (buffer-live-p source)
+ (user-error
+ "The dashboard's deck buffer is gone; run org-drill-statistics from a deck"))
+ (let ((body (with-current-buffer source
+ (org-drill-statistics--render-all scope range algorithm))))
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert body)
+ (goto-char (point-min)))
+ (setq org-drill-statistics--scope scope
+ org-drill-statistics--range range
+ org-drill-statistics--algorithm algorithm
+ org-drill-statistics--source-buffer source)
+ (setq buffer-read-only t)
+ buffer))))
;;;###autoload
(defun org-drill-statistics ()
@@ -5748,13 +5797,14 @@ filter header and the five render sections, using the current
to it. Refresh and the s/r/a cycle commands re-render in place."
(interactive)
(let ((buffer (get-buffer-create org-drill-statistics--buffer-name))
+ (source (current-buffer))
(scope org-drill-scope)
(range (caar org-drill-statistics-range-presets))
(algorithm nil))
(with-current-buffer buffer
(org-mode)
(org-drill-statistics-mode 1))
- (org-drill-statistics--render buffer scope range algorithm)
+ (org-drill-statistics--render buffer scope range algorithm source)
(switch-to-buffer buffer)
buffer))
@@ -5767,7 +5817,8 @@ every section against the current log and card state. Bound to g."
(current-buffer)
org-drill-statistics--scope
org-drill-statistics--range
- org-drill-statistics--algorithm))
+ org-drill-statistics--algorithm
+ org-drill-statistics--source-buffer))
(defun org-drill-statistics-cycle-scope ()
"Cycle the dashboard scope filter and refresh.
@@ -5990,7 +6041,15 @@ do. Interactively, prompts for DIRECTORY."
(org-drill-statistics--write-csv
(expand-file-name "cards.csv" directory)
org-drill-statistics--cards-csv-header
- (org-drill-statistics--cards-rows scope))
+ (let ((source org-drill-statistics--source-buffer))
+ (cond
+ ((buffer-live-p source)
+ (with-current-buffer source
+ (org-drill-statistics--cards-rows scope)))
+ (source
+ (user-error
+ "The dashboard's deck buffer is gone; refresh from a deck first"))
+ (t (org-drill-statistics--cards-rows scope)))))
(org-drill-statistics--write-csv
(expand-file-name "daily.csv" directory)
org-drill-statistics--daily-csv-header