aboutsummaryrefslogtreecommitdiff
path: root/org-drill.el
Commit message (Collapse)AuthorAgeFilesLines
* feat: optionally show the card's outline path in the drill promptHEADmainCraig Jennings14 days1-1/+23
| | | | | | A new defcustom org-drill-show-outline-path-during-drill (default off) prepends the card's ancestor path to the mini-buffer prompt, for example [Spanish > Greetings], so during a drill you can see where the current card sits in the deck. With it off the prompt is byte-for-byte unchanged. I ported this from m.galimski's fork (commit c6d0c850) and gated it behind the defcustom rather than leaving it always on. The path comes from org-get-outline-path through a small org-drill--outline-path-string helper. Tests cover the helper (nested and top-level), the prompt with the switch on and off, and the default value.
* feat: add statistics dashboard CSV export and docsCraig Jennings14 days1-1/+182
| | | | | | | | org-drill-statistics-export-csv, bound to e in the dashboard and now implemented, writes three files into a chosen directory honoring the active scope and range: sessions.csv (one row per recorded session), cards.csv (one row per drill card in scope with its scheduling properties and computed status), and daily.csv (per-day reviews, passes, fails, pass percent, and duration). Fields are quoted per RFC 4180 by a small csv-quote helper, since csv-mode isn't a dependency. This is the dashboard's last piece, step 3 of the spec. The row builders for the three views are pure and unit-tested with deterministic fixtures, and csv-quote covers the comma, quote, and newline cases. I documented the dashboard and the export in org-drill.org (a new section with the keymap, the CSV columns, and the settings table) and added a feature bullet to the README. I also removed the now-redundant declare-function forward reference for export-csv. It named this file as the source, so once the real defun landed the byte-compiler counted the function twice and warned.
* feat: add the org-drill statistics dashboard rendererCraig Jennings14 days1-0/+1245
| | | | | | | | | | Step 1 shipped the session-log data layer. This is the renderer on top of it. org-drill-statistics opens a read-only dashboard with five sections: an overview (card counts plus a last-session recap), trends (reviews-per-day and pass-rate-per-day quadrant-block sparklines over the trend window, plus a 12-week table), a quality histogram, a needs-attention view (leech candidates, long-overdue, and forgotten-new cards), and a 7-day forecast counted from SCHEDULED dates. A buffer-wide filter (scope, range, algorithm) sits in the header and cycles with s/r/a. The other keys are q to bury, g to refresh, e for the CSV-export hook that lands next, and RET to follow the card link at point. The aggregation math lives in pure helpers (day-bucketing, sparkline scaling, weekly aggregates, the histogram, the attention selectors, forecast bucketing). The render helpers are thin string formatters over them, so the logic is unit-tested independently of the UI. New defcustoms tune the views: org-drill-statistics-trend-days, -forecast-days, -attention-row-limit, and -leech-quality-threshold. I added require 'calendar for the Monday week-start arithmetic in the weekly aggregates. CSV export and the manual and README entries are the step-3 follow-on.
* feat: add org-drill-treat-headline-as-card-p for empty-bodied cardsCraig Jennings14 days1-2/+19
| | | | | | | | A drill entry with an empty body is skipped unless its card type opts into empty bodies via the DRILL-EMPTY-P slot of org-drill-card-type-alist. That left no global way to drill headline-only items, or hierarchical-notes decks where the heading is the question and the answer lives in child entries (upstream #30, #41). I added org-drill-treat-headline-as-card-p, default nil so existing decks are unchanged. When it's on, the empty-skip gate in org-drill--entry-empty-and-not-empty-friendly-p short-circuits, so every empty-bodied entry is drilled with its heading as the question regardless of card type. I added the safe-local-variable booleanp declaration alongside the other booleans and documented the switch in org-drill.org. Tests pin the predicate and the classify-status outcome both on and off, and confirm the per-card-type DRILL-EMPTY-P path stays independent of the new switch.
* fix: detect #+FILETAGS decks so org-drill-mode auto-enablesCraig Jennings2026-05-301-10/+27
| | | | | | | | org-drill-buffer-has-cards-p only scanned for a per-heading :drill:/:leitner: tag, so a deck tagged through #+FILETAGS: had no match and org-drill-mode never auto-enabled. Those files opened without cloze highlighting. I extended the predicate to also scan #+FILETAGS: lines, handling both the space-separated and colon-delimited syntaxes, with [: \t] boundaries so a value like drilldown can't false-match drill. The inherited-top-level-tag case already worked, since the ancestor heading line carries the literal tag and the per-heading scan catches it. Tests cover filetag-only decks (space, colon, leitner), the inherited-top-level lock, the substring boundary, and auto-enable on a filetag-only buffer.
* docs: relocate v0 design specs to docs/design/Craig Jennings2026-05-281-4/+3
| | | | | | | | | | | | | | | | I moved both v0 design specs out of working/ and into docs/design/. That's the conventional permanent home for project documentation, where engineers will look during implementation. working/ is meant for transient in-progress artifacts that file away once the work ships, and these specs are long-lived design docs that don't fit that contract. Files moved: - working/stats-dashboard/stats-dashboard.org → docs/design/stats-dashboard.org - working/fsrs-spec/fsrs-spec.org → docs/design/fsrs-spec.org The git rename detection picked both up, so file history follows the move. I also dropped the stale /docs entry from .gitignore. The Makefile doesn't write to docs/ and nothing else references it as a build output, so the ignore was inherited cruft that would have silently dropped any tracked file under docs/. I updated path references in seven spots: three docstring/comment refs in org-drill.el, one in tests/test-org-drill-session-record.el (the Commentary block), and three inside the specs themselves. Two refs in fsrs-spec.org now point at the correct location for its defcustom docstring and option description. One in stats-dashboard.org's References section points at the sister spec. Full make test-unit green. eask compile clean.
* feat: persist a session log for the stats dashboardCraig Jennings2026-05-281-8/+144
| | | | | | | | | | | | | | | | I added the persist + recording layer that the future stats dashboard reads, plus the v0 design spec at working/stats-dashboard/stats-dashboard.org that scoped the work. Every completed (non-suspended) drill session now appends one org-drill-session-record to a persisted org-drill-session-log via persist-defvar, mirroring the SM5-matrix pattern that's already in place. A new sibling defgroup org-drill-statistics groups the dashboard's customs. The record carries: start-time, end-time, scope, algorithm, qualities (as a vector), pass-percent, new-count, mature-count, failed-count, cram-mode. Scope and algorithm are captured at session-start time onto two new org-drill-session slots (scope-at-start, algorithm-at-start) so a mid-session defcustom flip doesn't misrepresent what was actually drilled. Both org-drill--prepare-fresh-session and the org-drill-again resume path populate the slots. I extracted pass-percent into org-drill--compute-pass-percent and call it from both org-drill-final-report and org-drill-session-record-from-session, so the user-visible report and the persisted record can't drift on the rounding or the failure-quality threshold. I wrapped recording in condition-case at the call site, not ignore-errors. Any recorder bug or persist-save IO failure gets messaged so silent data loss leaves a forensic trail. Corrupt-load recovery follows the SM5 path's condition-case fallback and adds an org-drill--session-log-quarantine helper that renames the bad file to a .corrupt-YYYY-MM-DDTHHMMSS sibling so the next save doesn't overwrite it. The seconds-granularity suffix prevents a same-day double corruption from clobbering the earlier quarantine. The helper depends on persist--file-location, an internal symbol guarded behind fboundp and documented inline. The spec at working/stats-dashboard/stats-dashboard.org is ratified, with all 10 originally-open decisions resolved in the Ratified Decisions table at the bottom: persist-defvar, warn-and-rename corrupt-load recovery, quadrant-block sparklines, a single buffer-wide filter, CSV with proper quoting, sync dashboard open, defer aborted-session recording, the q/g/e/s/r/a/RET keymap, leech-quality threshold default 2.5, and a sibling org-drill-statistics defcustom group. The remaining work (dashboard renderer, CSV export, docs) lands in follow-up commits. I followed TDD throughout. 24 tests in tests/test-org-drill-session-record.el cover struct construction, the shared pass-percent helper, the builder including scope/algorithm capture and mature-count summing, log appending with newest-first ordering and persist-save call-through, the symbol-bound smoke check, the quarantine rename plus its seconds-granularity contract, and the end-message hook (records on normal completion, skips on suspend, logs to message on recorder error). Full make test-unit green. eask compile clean.
* feat: change default scheduling algorithm to simple8 with ADRCraig Jennings2026-05-271-2/+51
| | | | | | | | Closes upstream #46. I changed the default for org-drill-spaced-repetition-algorithm from sm5 to simple8. The defcustom now carries an ADR-style comment recording the reasoning: simple8 gives most of sm5's per-user adaptation value via its per-card difficulty learning, without sm5's dependency on a persisted optimal-factor matrix that can rot (the upstream #45 fragility), and it adjusts intervals for early or late reviews, a real-world concern sm2 and sm5 don't address. Existing users with the option set in their config see no change. Existing users on the previous default get simple8 on their next session, with no state migration. Simple8 reads what it can from the SM-style item-data and carries on. Sm5's persisted optimal-factor matrix stays on disk and is available if the user switches back. I added a test that pins the default value, so an accidental flip surfaces in CI.
* refactor: take card-state in org-drill-determine-next-interval-simple8Craig Jennings2026-05-271-25/+14
| | | | | | | | Stage 5 of #147, closing the scheduler migration. simple8 now takes (state quality &optional delta-days) instead of seven positional args, binding the recall fields from the struct at the top so the algorithm body is unchanged. simple8 doesn't use ease, so the binding skips that slot. Both call-site branches collapse to (state quality [delta-days]), dropping the per-branch accessor unpacking. The testutil adapter test-scheduler--call-simple8 keeps the simple8 test calls a one-symbol rename per site. One direct simple8 call in tests/test-org-drill-prompt-and-format-helpers.el now uses the new struct API inline. With this stage landed, all three schedulers, the item-data round-trip, and every test caller go through the org-drill-card-state struct, finishing #147.
* refactor: take card-state in org-drill-determine-next-interval-sm5Craig Jennings2026-05-271-19/+15
| | | | | | | | Stage 4 of #147. sm5 now takes (state quality of-matrix &optional delta-days) instead of nine positional args, binding the recall fields from the struct at the top so the algorithm body is unchanged. Both call-site branches pass the state they already hold, dropping the per-branch accessor unpacking. The testutil adapter test-scheduler--call-sm5 keeps the sm5 test calls a one-symbol rename per site. I also kept the return as the existing list, matching the stage-3 refinement: the goal is reducing the input signature, and changing the return shape would force the shared return-extractors and every return-read to change for no real gain. Also folds in two stage-3 follow-ons I missed when sm2 landed: a direct sm5 call in tests/test-org-drill-small-branch-coverage.el now uses the new struct API inline, and five direct sm2 calls in the simple-workflow integration test now go through the testutil adapter (the integration file picks up the testutil-scheduler require). Caught by running make test-integration this stage, which I should have run on the sm2 stage.
* refactor: take card-state in org-drill-determine-next-interval-sm2Craig Jennings2026-05-271-22/+15
| | | | | | Stage 3 of #147. sm2 now takes (state quality) instead of seven positional args, binding the recall fields from the struct at the top so the algorithm body is unchanged. The smart-reschedule and hypothetical-next-review-date call sites pass the state they already hold, which drops the per-branch accessor unpacking. I kept the return as the existing positional list rather than restructuring it too. The goal is reducing the input signature, and changing the return shape would force the shared test extractors and every return-read to change for no real gain. A testutil adapter, test-scheduler--call-sm2, packs positional args into the struct, so the test call sites stay readable as the documented algorithm inputs and the migration is a one-symbol rename per call.
* feat: add org-drill-on-timeout-action to drop unfinished cards at the time limitCraig Jennings2026-05-271-11/+32
| | | | | | Implements upstream #56. When org-drill-maximum-duration is reached, the session used to keep presenting the in-progress card and the again-queue until they drained, so the only way out was to finish them or interrupt. I added the defcustom org-drill-on-timeout-action with a discard-current value that ends the session as soon as the limit is hit, leaving the dropped cards untouched: they keep their existing scheduling and turn up again next session. The default, finish-current, preserves the old behavior. The gate lives in org-drill-entries-pending-p, the single predicate the drill loop checks between cards. Under discard-current past the limit, the in-progress and again items stop counting as pending, so the loop ends instead of draining them.
* refactor: thread card-state struct through the item-data round-tripCraig Jennings2026-05-271-93/+129
| | | | | | | | Second step of #147. get-item-data now returns an org-drill-card-state and store-item-data takes one, so the six recall fields move as named slots instead of a positional list. The three call sites (smart-reschedule, hypothetical-next-review-date, copy-scheduling-to-marker) read scheduler inputs through accessors and build a struct for the store, which removes the hand re-ordering between the get-shape and the store-shape. Behavior is unchanged. The legacy LEARN_DATA read path and the virgin-item sentinel are preserved field-for-field, and store takes just the struct because its last-interval slot already holds the interval to persist. The schedulers still take positional args; they adopt the struct in the following commits. I updated the round-trip, integration, and setup-helper tests to build and read the struct via a small list-view helper, so the existing expected-value assertions stay readable.
* refactor: add org-drill-card-state struct + ADRCraig Jennings2026-05-271-0/+22
| | | | | | First step of #147. I added a cl-defstruct bundling the recall fields the schedulers and the item-data round-trip shuttle around (last-interval, repetitions, ease, failures, meanq, total-repeats), with an ADR comment recording why: the same fields were passed as positional lists in three different orderings, and three call sites re-ordered between them by hand. The struct is inert here. The item-data round-trip and the schedulers adopt it in the following commits.
* refactor: sharpen opaque local variable names across org-drill.elCraig Jennings2026-05-271-39/+39
| | | | | | I renamed several cryptic locals to say what they hold: idx to index-var in the pop-random gensym, val to raw-value in the five DRILL_* property getters, q to quality in hypothetical-next-review-dates, m to marker in free-markers, the a/b sort and filter lambdas in order-overdue-entries to entry/other, and dat to card-def in the empty-card-type check. These are pure renames with no behavior change. Byte-compile and the full unit suite stay green.
* refactor: name the progress-meter chars in org-drill-progress-messageCraig Jennings2026-05-271-4/+4
| | | | sym1/sym2 gave no hint of their role. current-meter-char toggles per meter wrap and alternate-meter-char is its inverse, which is what the two make-string calls actually consume.
* feat: undo last rating, customizable keys, and configurable text limitCraig Jennings2026-05-271-49/+142
| | | | | | | | | | | | | | A batch of self-contained user-facing improvements, squashed from the feat/org-drill-solo-features branch. I added an undo for the last rating (issue #2 follow-up). The rating prompt now takes an undo key (org-drill--undo-key, default u): it restores the previous card's scheduling snapshot, drops the recorded quality, and re-queues that card, then returns to the current prompt. Each rating snapshots the scheduling properties and SCHEDULED line onto a per-session stack capped at org-drill-undo-limit (default 3). org-drill-reschedule loops on the rating read so undo doesn't rate the current card. I made the five session-control keys (quit, edit, help, skip, tags) defcustoms so they can be rebound from customize-group (issue #35), keeping their defaults. The 0-5 rating keys stay as-is, since they're tied to the quality scale rather than being variables. I lifted the hardcoded 100-line entry-text limit in org-drill-get-entry-text into the org-drill-entry-text-max-lines defcustom, defaulting to 100. I also deleted a commented-out old org-entry-empty-p that the real definition had already replaced. Existing tests stay green and each change added its own, including snapshot/restore and prompt-loop tests for undo.
* refactor: dedupe presenters, group defcustoms, and fill in docstringsCraig Jennings2026-05-261-232/+303
| | | | | | | | | | | | | | A cleanup pass over org-drill internals, squashed from the refactor/wave3-cleanup branch. No behavior change. Each step kept the existing tests green and added its own. I shared two duplicated helpers across the language card getters: org-drill--read-property-string and org-drill--face-from-alist. I factored the cloze body-scan out of the two multicloze presenters into org-drill--cloze-body-bounds, org-drill--count-cloze-matches, and org-drill--hide-cloze-by-index, so each presenter just picks which indices to hide. I pulled the presenter resolution and the four-way result classification out of org-drill-entry-f into org-drill--resolve-presenter and org-drill--classify-presentation-result, untangling the pivot of every drill iteration. I split the 37 defcustoms (and the three cloze faces) into four customize sub-groups (display, algorithm, session, leech) so customize-group org-drill is navigable. There's no leitner group because the Leitner settings are defvars. I documented the 22 defuns that had no docstring, rewrote the corrupted org-drill-presentation-prompt-in-mini-buffer docstring, and switched eleven docstrings to the imperative "Return" (issue #2).
* fix: scope cloze fontification to drill buffers via org-drill-modeCraig Jennings2026-05-261-10/+73
| | | | | | | | org-drill-add-cloze-fontification ran on org-font-lock-set-keywords-hook, which fires in every org buffer, and pushed the cloze rule into org's global org-font-lock-extra-keywords. The cloze regexp is built from the [ and ] delimiters, so an org priority cookie like [#A] matched the cloze pattern and got fontified as a cloze in every org buffer, colliding with org's headline fontification and stripping the heading's org-level-N face. I replaced the global install with org-drill-mode, a buffer-local minor mode that adds the cloze keywords only to its own buffer via font-lock-add-keywords. org-drill-auto-enable-mode (default on) turns the mode on from org-mode-hook in buffers that hold drill cards, so existing drill files keep their cloze highlighting while plain org buffers stay clean. Highlighting still respects org-drill-use-visible-cloze-face-p. The cloze regexp itself is unchanged, so the single-line cloze constraint from #38 is preserved.
* chore: coverage, autoload fix, and internal cleanup for org-drillCraig Jennings2026-05-261-37/+68
| | | | | | | | | | | | | | | | A batch of test-coverage and hardening work, squashed from the test-work branch. Tests: deduplicated a colliding leitner-capture test name so make test-name loads again. Added SM2 assert-failure cases, the six basic multicloze variant delegations, the three English-side spanish-verb branches, and org-drill-current-scope branch coverage. Fix: the entry-point commands (org-drill itself, cram-tree, tree, directory, resume, relearn-item, strip-all-data, merge-buffers) carried no autoload cookies, so M-x failed from a fresh install until something pulled the file in. They're autoloaded now. Perf: org-drill-shuffle was quadratic because it indexed a list with elt on every swap. It runs a linear Fisher-Yates pass over a vector now, and it checks its argument is a list. Feat: added org-drill-version, a constant plus an interactive command, so a bug reporter doesn't have to open the file header. Refactor: extracted org-drill--format-tense-mood, shared by the two verb-conjugation presenters that each carried a copy. Docs: explained the SM8 magic numbers in the simple8 helpers as empirical fits rather than tunable knobs.
* build: migrate from Cask to EaskCraig Jennings2026-05-101-1/+1
| | | | | | | | | | Cask's upstream has slowed. Eask is the actively maintained successor. Eask's `package-file` directive doesn't auto-install the deps from the .el header's Package-Requires, so the Eask file mirrors emacs/seq/org/persist explicitly. `eask install-deps` also doesn't pull transitive deps, so dash, m-buffer, and shut-up needed their own `depends-on` lines for undercover and elisp-lint to activate. The Makefile swaps are mechanical: $(CASK) → $(EASK), `cask install` → `eask install-deps --dev`, `cask build` → `eask compile`. The URL in org-drill.el's header pointed at the abandoned upstream's GitLab issues page. Eask cross-validates that against website-url, so I updated it to the GitHub mirror — where users file issues now.
* refactor: split org-drill-entry-status classifier from its predicatesCraig Jennings2026-05-051-38/+34
| | | | | | | | | | | | | | | | | Two extractions out of the 58-line cond: - org-drill--entry-empty-and-not-empty-friendly-p: encapsulates the ugly nested predicate that means 'body is empty AND the card type doesn't opt in to empty bodies'. Used to be 5 inlined lines including a let* and an alist lookup. - org-drill--classify-status: takes the precomputed DUE and LAST-INT and walks the cond. The decision tree is now a flat sequence of one-line clauses. org-drill-entry-status itself drops to 8 lines and reads as 'compute the inputs, classify, return triple'. The :failed branch also uses org-drill--quality-failed-p instead of inlining the threshold check.
* refactor: split org-drill-merge-buffers into named phasesCraig Jennings2026-05-051-64/+74
| | | | | | | | | | | | | | | | | | | | | | merge-buffers was 89 lines of dense cross-buffer marker work mixing hash-table population, source-buffer iteration with embedded property reads, scheduling-data writes, and cleanup of unmatched DEST entries. Extracted four helpers: - org-drill--build-dest-id-table: scan DEST and populate the id→ marker table. - org-drill--copy-scheduling-to-marker: read the current entry's scheduling state and write it at MARKER (skipping never-rated items via total-repeats=0 guard). - org-drill--migrate-from-source: walk SRC, dispatch to the copy helper or org-drill-copy-entry-to-other-buffer for new items. - org-drill--strip-unmatched-dest-entries: clean up DEST entries that have no SRC match. merge-buffers itself drops from 89 lines to 19 and reads as a sequence of named phases.
* refactor: split org-drill main entry into named phasesCraig Jennings2026-05-051-99/+103
| | | | | | | | | | | | | | | | | | | | | org-drill was 137 lines mixing five distinct concerns: an org-version warning (now dead under the org>=9.6 floor), session reset, entry collection, drill execution, and post-session messaging. Extracted four helpers, each with a single responsibility: - org-drill--prepare-fresh-session: zero out queues + counters - org-drill--collect-entries: scan + sort overdue - org-drill--queues-empty-p: predicate for the no-pending branch - org-drill--show-end-message: dispatch resume-hint vs final-report Plus org-drill--show-resume-hint for the keystr-aware suspended message. Removed the dead org<7.9.3f warning block (the org>=9.6 floor makes it unreachable). org-drill itself drops from 137 lines to 36 and the cl-block wrapper goes away — the cl-return-from inside org-drill-entries returns through the normal control flow now.
* refactor: split org-drill-final-report into format helpersCraig Jennings2026-05-051-73/+71
| | | | | | | | | | | | | | | | | | | | org-drill-final-report was 95 lines, dominated by two big format calls (the main summary and the low-pass-rate WARNING) plus an inlined queue-tag pattern that propertized 5 different counts in the same shape. Extracted three helpers: - org-drill--queue-tag: builds a propertized 'N label' string for one queue (failed / overdue / new / young / old). Replaces 5 inlined propertize calls. - org-drill--build-final-report-summary: takes the session, and returns the formatted main summary string. - org-drill--build-low-pass-warning: takes the session and pass- percent, returns the formatted warning string. org-drill-final-report becomes a 12-line orchestrator that wires the helpers together with the wait-and-read loop.
* refactor: flatten nesting in org-drill-entriesCraig Jennings2026-05-051-35/+50
| | | | | | | | | | | | | | | | | | | The main loop body reached 7 levels of indentation in the success path: while > destructuring-bind > save-excursion > cond > t-clause > let > cond > inner-cond. Extracted two helpers: - org-drill--pick-next-marker: chooses between resuming the current-item slot and popping a fresh marker, returning a (marker . next-resuming-p) cons so the caller updates resuming-p in lock-step. - org-drill--route-rating-result: routes the rating result into the session's again/done queues and returns a symbol (quit/edit/ skip/next) telling the caller whether to break the loop. org-drill-entries shrinks from 54 lines to 19, and the deepest nesting drops from 7 levels to 4.
* refactor: extract org-drill--read-rating-key shared by reschedule and ↵Craig Jennings2026-05-051-66/+48
| | | | | | | | | | | | | | | | | | | | | | leitner-rebox The two interactive rating loops (reschedule and leitner-rebox) shared roughly 60 lines of identical code: the same key-prompt string, the same memq-based exit-on-rating loop, the same arrow/ scroll/wheel cond, the same tags-key dispatch, and the same help-key toggle. Only two things differ — the rating-explanation help text and the per-rating action that follows — and the comment in leitner-rebox literally said "All this is shared with drill- reschedule. And what does it do?" Extracted org-drill--read-rating-key with two args: typed-answer (for the typed-answer flow's 'Your answer: ...' line) and rating-help-block (the multi-line ratings explanation specific to the scheduler). Both call sites collapse from ~50 lines of inlined loop to a single call. reschedule and leitner-rebox now consist of just the rating- specific actions plus this read-key call.
* refactor: introduce org-drill-with-card-display macroCraig Jennings2026-05-051-60/+60
| | | | | | | | | | | | | | | | | | | Five presenters opened with the same three-deep wrap: (org-drill-with-hidden-comments (org-drill-with-hidden-cloze-hints (org-drill-with-hidden-cloze-text ...body...))) Combined into org-drill-with-card-display. Five sites (present-simple-card, present-simple-card-with-typed-answer, present-two-sided-card, present-multi-sided-card, present-spanish-verb) lose 2-3 lines of nesting each. Multicloze-hide-n / hide-nth use a different envelope (only two of the three wraps; they hide specific clozes by index, not all of them) so they keep their explicit nesting. Same for present-card-using-text, which substitutes with-replaced-entry-text for with-hidden-cloze-text.
* refactor: extract org-drill--quality-failed-p predicateCraig Jennings2026-05-051-5/+11
| | | | | | | | | | | | The check (<= quality org-drill-failure-quality) appeared in five places: SM2/SM5/Simple8 schedulers, smart-reschedule, and the main org-drill-entries loop body. Each call site does different things on failure (reset interval, push to again-entries, etc.) so only the predicate was duplicated. Extracted as org-drill--quality-failed-p with a docstring naming the threshold and the role. Five inlined comparisons collapse to five named predicate calls.
* refactor: extract LEECH-warning preamble helperCraig Jennings2026-05-051-24/+19
| | | | | | | | | | | The seven-line propertize+concat block that prepends a red leech warning to the prompt was inlined in three prompt builders: presentation-prompt-in-mini-buffer, presentation-prompt-in-buffer, and presentation-prompt-for-string. Extracted org-drill--maybe-prepend-leech-warning as a single helper the three call. 21 lines duplicated → one definition + three one-line call sites.
* refactor: remove smaller commented-out alternative branchesCraig Jennings2026-05-051-18/+0
| | | | | | | | | | | | | | | | Four dead-code blocks deleted, all of them commented-out alternative implementations next to the live versions: - 5-line commented (and (>= quality 4) ...) cond branch in the SM5 scheduler - 6-line commented (loop do (re-search-forward ...)) alternative in present-multicloze-hide-n - 4-line commented unless-error guards at the top of entry-f - 3-line commented (:tomorrow ...) case branch in map-entry-function Prose commentary kept (license, package overview, recent-fix explanations). Pure deletion, no behavior change.
* refactor: convert spanish-verb 6-way cl-case to alist dispatchCraig Jennings2026-05-051-47/+19
| | | | | | | | | | | | | org-drill-present-spanish-verb had a 6-branch cl-case where each branch differed only in two values: which subheading to reveal (Infinitive or English) and which prompt string to show. Inlined into a 50-line block of nearly-identical setq calls. Extracted the (reveal . prompt) pairs into a defconst alist and reduced the dispatcher to a random-pick + apply-pair pattern. Function drops from 51 lines to 14. Existing branch tests still pass — they mock cl-random to a specific index, and the alist's order matches the previous case order.
* refactor: extract org-drill--quality-percent helper for final-reportCraig Jennings2026-05-051-12/+11
| | | | | | | | | | | | The quality-percentage formula (round (* 100 (cl-count Q qualities)) (max 1 (length qualities))) appeared six times in org-drill-final-report (once per recall quality 0..5), each time inlined verbatim. Extracted to a single helper. Six call sites collapse from three lines each to one. Behavior identical (the helper documents the (max 1 ...) divisor as an empty-list guard). Final-report drops from 95 lines to ~80.
* refactor: drop Org <9.6 fallback in time-to-inactive-org-timestampCraig Jennings2026-05-051-6/+1
| | | | | | | | | | | The function had an Org-version branch with a legacy <9.6 path that used (substring (cdr org-time-stamp-formats) 1 -1). Modern Org's format-strings dropped the angle brackets that the substring slice assumed, so the legacy path was both dead-code (unreachable under the org>=9.6 dep floor we just declared) and silently buggy if it ever did run. Function is now a one-liner around the modern primitive.
* refactor: drop Org <9.6 compat shimsCraig Jennings2026-05-051-23/+0
| | | | | | | | | | | | | Two dead branches removed: - (when (version< org-version "9.2") ...) advice on org-get-tags, plus the org-drill-get-tags-advice helper that backed it. Org 9.2 shipped the new arity in 2018 — well below our org>=9.6 floor. - (when (= 8 (car ...)) ...) Org 8.x defalias shim that wrapped org-latex-preview around org-preview-latex-fragment. Org 8 hasn't been a target for years and is below the org>=9.6 floor. Both are now genuinely dead after the dep bump in 75b1601.
* refactor: delete commented-out function bodiesCraig Jennings2026-05-051-58/+0
| | | | | | | | | | | | | | Three large dead-code blocks removed: - 16-line commented-out org-drill-entry-due-p (replaced by current implementation that takes a session arg) - 17-line commented-out org-drill-hide-all-subheadings-except (the body was a placeholder docstring + commented body; the real function lives elsewhere now) - 25-line block of commented-out alternative org-drill-add-cloze-fontification + add-hook XXX commentary Pure deletion, no behavior change. Version control has the history.
* build: bump Org dep to 9.6 to match unguarded org-fold-* callsCraig Jennings2026-05-051-1/+1
| | | | | | | | | | | | | | | | | org-drill calls org-fold-show-entry and org-fold-show-subtree from seven sites without fboundp guards. Both functions arrived in Org 9.6. But the package declared org 9.3 (Package-Requires) / org 9.2 (Cask), so users on older Org would silently void-function at runtime instead of getting a clear install-time mismatch error. Bumped both declarations to org 9.6. Wrapping each of the seven call sites with fboundp would be the alternative, but Org 9.6 was released October 2022 — three-and-a-half years ago — and we already have a follow-up TODO to drop the legacy time-to-inactive fallback that this version bump unblocks. Two tests verify the declared dep and that the org-fold APIs are actually bound on the running Org version.
* fix: keep collection scan alive when one entry errors (upstream #53)Craig Jennings2026-05-051-24/+34
| | | | | | | | | | | | | | | | | | | | | | User reported that running org-drill on a buffer with a new (no-ID) entry threw 'Wrong Type Argument: hash-table-p, nil' and stopped the scan — every subsequent entry was silently skipped, so the user had to re-run org-drill once per item (10 items meant 10 invocations). The exact source of the hash-table error is environment-dependent (Emacs version, Org version, lazy org-id-locations init, Doom overrides), so this fix targets the user-visible failure mode instead of the underlying triggering condition. Wrapped the per-entry body of org-drill-map-entry-function in condition-case. An error on one entry now logs a 'skipping' message and the scan continues to the next entry. The session collects all the well-formed items, and the user can re-run drill once total to process them — no more once-per-item. Two regression tests: one verifies the resilience behavior directly (fail entry 1, scan continues to entry 2), the other documents the ID-creation-with-uninitialized-locations scenario as a smoke check.
* fix: keep cloze regex within a single line (upstream #38)Craig Jennings2026-05-051-2/+9
| | | | | | | | | | | | The inner match was [[:cntrl:][:graph:][:space:]]+?, which silently includes newline. A stray [ could match all the way to a ] several lines later, covering org headings in between with the visible-cloze face. Reporter saw lines 4 and 5 of test.org lose their org-level-N face and use default instead. Switched the inner class to [^\n]+?. Clozes now stay within a single line, which matches the design intent and stops the face bleed. Three new tests cover the regression.
* fix: skip LaTeX preview on TTY frames (upstream #44)Craig Jennings2026-05-051-10/+18
| | | | | | | | | | | | | | | | | Issue #44 (2021): running org-drill in a TTY emacsclient (the reporter mentioned tmux) raised "Window system frame should be used" because LaTeX preview helpers (org-latex-preview, org--latex-preview-region) require a window system and weren't guarded. Wrapped both call sites with (when (display-graphic-p) ...). - org-drill--show-latex-fragments: now a silent no-op on TTY - present-default-answer's clear-and-preview block: same guard LaTeX previews are inherently graphical. The right behavior on TTY is to skip the preview rather than crash the session — TTY users still see the underlying source text just fine.
* fix: clear stale end-pos on resume so final-report fires (upstream #33)Craig Jennings2026-05-051-0/+7
| | | | | | | | | | | | | | | | When a user interrupted a drill session to edit or capture, the session's end-pos slot got set to a marker (or :quit). The end-of- org-drill cond branched on end-pos: if set, show resume message and skip org-drill-final-report. That worked for the first interruption. But on org-drill-resume, the session was reused with end-pos still carrying the prior marker. Even when the resumed session completed normally, the same cond branch fired again — silently skipping final-report. Clear end-pos at the top of org-drill when resume-p is non-nil, per Markus's proposed patch on the upstream issue. The resumed session can now reach the final-report branch.
* fix: restore display state in the buffer setup ran inCraig Jennings2026-05-051-20/+40
| | | | | | | | | | | | | | | | | | org-drill--setup-display saved buffer-local state (mode-line, variable-pitch-mode) into global defvars and called setq-local on the current buffer. org-drill--restore-display read those globals and ran setq-local against whatever buffer happened to be current at restore time. If the user switched buffers mid-session, the restore wrote to the wrong buffer — leaving the original drill buffer's mode-line still hidden and trampling the destination buffer's mode-line with whatever was saved from elsewhere. Captured the buffer at setup in org-drill--saved-display-buffer. Restore now wraps mode-line and variable-pitch restoration in with-current-buffer against that saved buffer. Text-scale stays global (the underlying face attribute is process-wide).
* fix: recover from corrupted persist file at package load (upstream #45)Craig Jennings2026-05-051-3/+14
| | | | | | | | | | | | | Issue #45 (2021): persist-load raised End of file during parsing at persist.el:413 in some configurations, likely from a corrupted persist data file. Pre-fix, this propagated up through the top-level (persist-defvar org-drill-sm5-optimal-factor-matrix ...) form at file-load time and broke the entire package's load. Wrapped the persist-defvar form in condition-case. On failure, the matrix falls back to a fresh nil binding via plain defvar, and a message tells the user what happened. org-drill continues to load normally.
* fix: guard org-drill-again and org-drill-resume against nil last-sessionCraig Jennings2026-05-051-0/+4
| | | | | | | | | | | | Both functions bound session to org-drill-last-session and immediately called setf / org-drill-entries-pending-p on it without checking for nil. First-time invocation (or after Emacs restart with no active session) threw an obscure eieio-oset / nil-slot type error instead of a clear message. Added (unless session (user-error ...)) at the top of each function. A user running M-x org-drill-resume cold now sees a sensible message telling them to run org-drill first.
* fix: hide-drawers ignores drawers with no :END:Craig Jennings2026-05-051-3/+10
| | | | | | | | | | | | | drawer-end was captured as (save-excursion (re-search-forward ':END:' end t) (point)) which always returns a number — (point) is always defined. The subsequent (when drawer-end ...) guard was dead, so a malformed drawer (typo in :END:, mid-edit truncation) ended up with a junk overlay covering whatever range point happened to land in. Captured the search result itself and gate on it. Malformed drawers are now skipped silently; well-formed drawers still get their normal overlay.
* fix: default DRILL_LEITNER_BOX to 0 in leitner-reboxCraig Jennings2026-05-051-1/+6
| | | | | | | | | | | When the property is absent, org-entry-get returns nil and string-to-number errors with wrong-type-argument. Reachable when a user removes the property mid-session, or when a Leitner-tagged entry is rebox'd before its DRILL_LEITNER_BOX has been set. Wrapped the org-entry-get with (or ... "0"). Box 0 makes the rating semantics still sensible: a downgrade stays at 0, a promotion goes to 1.
* fix: drop dead translate_number entry from card-type alist (upstream #43)Craig Jennings2026-05-051-2/+1
| | | | | | | | | | | | | | | | | | | The card-type alist mapped translate_number to a function that no longer exists in the file. Cards with DRILL_CARD_TYPE: translate_number crashed with void-function during drill instead of being skipped. Reporter (issue #43, 2021) said they had old decks using the documented translate_number type and were getting the crash on restore. The function was apparently removed at some point without clearing the alist entry. Removed the alist entry so entry-f's no-presentation-fn branch fires and returns skip after messaging the user. Legacy decks now degrade gracefully instead of crashing the session. Tests in tests/test-org-drill-translate-number-regression.el lock the behavior in (entry-f returns skip on translate_number, alist no longer carries the entry).
* fix: guard zero-divisor in org-drill-final-report overdue percentageCraig Jennings2026-05-051-2/+7
| | | | | | | | | | | | The warning branch divided 100*overdue by (dormant+due) without guarding the denominator. When both counts are zero — degenerate scopes (cram with no items collected, pure-failure session on empty queues) — the call hit arith-error before the warning even rendered. Wrapped the divisor with (max 1 ...). In the zero case the percentage reads as 0% rather than crashing the session wrap-up. Resolves a long-standing pre-existing TODO entry.
* fix: remove stray [debug] message in org-drill-entriesCraig Jennings2026-05-051-1/+0
| | | | | | | Every drilled card was logging "[debug] org-drill: at marker position N" to *Messages* and flashing it in the minibuffer. Pure noise — the print statement was clearly a leftover from diagnostic work that never got cleaned up. Delete it.
* fix: guard org-drill-smart-reschedule cond against nil days-aheadCraig Jennings2026-05-051-5/+10
| | | | | | | | | | | | | | | | The function takes `days-ahead' as &optional, but the schedule cond called `(= 0 days-ahead)' and `(cl-minusp days-ahead)' before any type-guard, so passing nil crashed with a wrong-type-argument error. Today's two callers (the rating-confirmation flow and the org-drill-relearn-item helper) always pass a number, so this was latent — but a third caller relying on the documented &optional shape would hit it immediately. Switched the cond to require numberp before the value comparisons, and the default branch now falls back to the algorithm-computed next-interval when days-ahead is nil. That matches the intent implied by the optional signature and the docstring.