vultan 1.0.0

Terminal-based, Anki-flavoured spaced-repetition study tool that reads flashcards from a directory of markdown notes.
Documentation

Vultan

A terminal-based, Anki-flavoured spaced-repetition study tool that reads your flashcards directly from a directory of markdown notes.

Vultan keeps your decks where your knowledge already lives - a zettelkasten or notes folder - rather than in a proprietary database. Each # Question / # Answer block in a .md file becomes a card; review state (due dates, intervals, memorisation factor, review history, study streak) is persisted as a single RON file alongside the notes (.vultan.ron) so it travels with the source.

Features

  • Markdown is the source of truth. No proprietary database. Add or edit cards in any editor; vultan picks up changes on next refresh.
  • Configurable card format. The defaults match a Q/A markdown layout, but any combination of tagged-line / wrapped-multi-line markers and deck delimiter is editable from in-app settings (no hand-editing of config files).
  • TUI dashboard with per-deck due/week/mastered/forgotten/total counts, 30-day review-activity and upcoming-due sparklines, and study streak.
  • Tunable SRS per deck (pass / easy / fail coefficients) plus app-wide mastered/forgotten thresholds. SM-2 style with sane defaults.
  • Card-content search (F) finds any card by substring of its question or answer; opens the hit in $EDITOR or shows its revision history.
  • In-app card editing. Add a card with N (deck-tag frontmatter pre-filled), edit the current card during a review with E, no leaving the TUI.
  • Skip / Bury / Undo during study, plus a per-card history view (H) showing every past review's score, post-transform factor, and interval.
  • Anki interop. Import .apkg files (including modern zstd-compressed Anki 2.1.50+ exports); export your decks back to .apkg with optional --with-state to preserve factor/interval/due.
  • Safe persistence. A 3-generation ring-buffer backup of .vultan.ron on every save, so a bad write or manual edit is recoverable. Optional auto-commit to git after each session.
  • Daily review limit (cap session size to the most-overdue N cards).
  • Shell completions for zsh, bash, and fish via vultan completions <shell>.

Installing

From crates.io:

cargo install vultan

Or from a local clone of this repo:

cargo install --path .

Either form drops a vultan binary on your $PATH. (You can also run from source with cargo run --bin vultan -- <args>.)

Card format

By default vultan looks for cards shaped like this:

---
title: composite-number
tags: :math:algebra:
---
# Question
What is a composite number?
# Answer
A natural number that can be factored by at least one number other
than 1 and itself.
----
  • Decks are derived from tags: (split on :).
  • The question/answer markers are configurable - see below.
  • Files that don't match are listed under vultan problems and skipped (and are reachable via [P] on the dashboard for in-app triage).

The format is described by four settings on the card_parsing_config field of .vultan.ron, all four of which you can edit live from the dashboard's Global settings screen ([G]) - no need to hand-edit the file. The four fields are:

Field What it picks out
decks_pattern The line / block listing the card's decks (tags)
deck_delimiter How that line is split into individual deck names
question_pattern Where the card's question text starts and ends
answer_pattern Where the card's answer text starts and ends

decks_pattern, question_pattern, and answer_pattern are each one of two shapes:

  • Tagged-line - a single tag: prefix; everything to the end of the line is the value. Good for one-line fields.
  • Wrapped multi-line - an opening_tag and a closing_tag bracketing the field. Good for multi-line / multi-paragraph fields.

Configuring card parsing

There are two ways to change the parsing rules:

  1. In-app - open the dashboard, press [G], navigate to one of the Card parsing rows, press Enter. The pattern editor lets you toggle between Tagged-line and Wrapped with Tab and edit the literal strings; saving validates and writes back to .vultan.ron.
  2. Edit .vultan.ron directly - only if you really want to. The schema is RON; a malformed value will refuse to load and you'll see the error on the next run. Vultan keeps a ring of three backups (.vultan.ron.bak.0..2) on each save, so a bad write is recoverable.

Examples

Default (multi-line wrapped Q/A, colon-separated tags)

card_parsing_config: (
    decks_pattern:    TaggedLine(tag: "tags:"),
    deck_delimiter:   ":",
    question_pattern: WrappedMultiLine(opening_tag: "# Question", closing_tag: "# Answer"),
    answer_pattern:   WrappedMultiLine(opening_tag: "# Answer", closing_tag: "----\n"),
)
---
tags: :math:algebra:
---
# Question
What is a composite number?
# Answer
A natural number that has more divisors than 1 and itself.
----

Tagged-line Q/A, comma-separated decks

Good if you prefer terse one-liners and don't need multi-paragraph answers.

card_parsing_config: (
    decks_pattern:    TaggedLine(tag: "tags:"),
    deck_delimiter:   ",",
    question_pattern: TaggedLine(tag: "Q:"),
    answer_pattern:   TaggedLine(tag: "A:"),
)
---
tags: math, algebra
---
Q: What is a composite number?
A: A natural number that has more divisors than 1 and itself.

Note: TaggedLine only captures up to the end of the line - multi-line question / answer text won't survive. Use WrappedMultiLine if you need it.

Anki-flavoured Front/Back with horizontal-rule terminator

card_parsing_config: (
    decks_pattern:    TaggedLine(tag: "deck:"),
    deck_delimiter:   "/",
    question_pattern: WrappedMultiLine(opening_tag: "## Front", closing_tag: "## Back"),
    answer_pattern:   WrappedMultiLine(opening_tag: "## Back",  closing_tag: "***\n"),
)
---
deck: math/algebra
---
## Front
What is a composite number?

## Back
A natural number that has more divisors than 1 and itself.

***

The closing_tag for the answer needs some sentinel after the answer body (here: a horizontal rule on its own line); without one vultan can't tell where the answer ends.

Quick start

vultan                         # opens the dashboard (first run prompts for notes dir)
vultan study --deck-name math  # study a single deck directly
vultan study --deck-name all   # study every due card across decks
vultan list-decks              # plain decks + due counts
vultan stats                   # rich per-deck breakdown
vultan problems                # paths of unparsable cards

The first time you run vultan with no subcommand, it prompts you for your notes directory (Tab-completion supported). The path is saved to $XDG_CONFIG_HOME/vultan/config.ron (falls back to ~/.config/vultan/config.ron) so subsequent runs go straight to the dashboard.

You can override the saved path on any subcommand with --notes-dirpath. An explicit override is also persisted as the new default.

Dashboard

┌─ VULTAN ─────────────────────────────────────────────────┐
│  Notes:    /Users/you/Code/personal/zettelkasten/study   │
│  Cards:    128 (24 errors)                               │
│  Streak:   3d (best: 14d)                                │
│  Last 7d:  41 reviews                                    │
└──────────────────────────────────────────────────────────┘
┌─ DECKS ──────────────────────────────────────────────────┐
│  DECK            DUE   WEEK   MASTERED   FORGOTTEN  TOTAL│
│ ▶algebra          75      4         12           5     80│
│  math            117      6         18           7    125│
│  ...                                                     │
│  « all due »     117      6         30          12    128│
└──────────────────────────────────────────────────────────┘
Key Action
/, j/k Move selection
Enter Study the selected deck (Enter on « all due » ⇒ study every due card)
/ Filter decks by name (typing narrows the list)
N New card in the selected deck - opens $EDITOR on a fresh .md file with deck-tag frontmatter pre-filled
F Find card by content (substring on Q+A); Enter opens the match in $EDITOR, F2 opens the card's revision history
R Random card from the selected deck (read-only browse, doesn't affect SRS state)
P Open the Problems screen (unparsable cards)
, Open per-deck Settings (SRS coefficients)
G Open Global settings (notes dir, daily review limit, auto-commit, card parsing)
F5 Re-read state from disk (pick up changes made externally)
? Toggle keybinds overlay
Q / Esc Quit

Study view

The session-info side panel shows live counters: cards passed, in-session streak, elapsed time, reviews-per-minute pace, and the source .md file of the current card.

Question mode:

Key Action
A Reveal answer
E Edit current card in $EDITOR
S Skip - push to back of queue
B Bury - defer this card until tomorrow
U Undo last score
H View this card's revision history (per-review score + factor + interval)
? Toggle keybinds overlay
Q Quit (with confirmation)

Answer mode shows the predicted next-review interval next to each scoring key:

Key Action
1 Fail
2 Hard
3 Pass
4 Easy

Problems screen

Reachable with P from the dashboard.

Key Action
/, j/k Move selection
E Open the selected file in $EDITOR
M Move all unparsable files to a directory (Tab-complete the path)
Q / Esc Back to dashboard

A summary of each move (moved 19 of 21) appears in-screen - no stderr smearing across the alt-screen.

Per-deck Settings (,)

Reachable with , from the dashboard for the currently-selected deck. Edits SRS coefficients and mastered/forgotten thresholds for that deck. App-wide settings live in Global settings (G) instead.

Key Action
/, j/k Move between fields
/ or -/+ Adjust value
R Reset field to global default
Enter Save (writes to .vultan.ron)
Esc Cancel

Fields: Pass coefficient, Easy coefficient, Fail coefficient, Mastered: factor, Mastered: interval, Forgotten: factor.

Global settings (G)

The unified hub for app-wide settings. From the dashboard press [G].

┌─ GLOBAL SETTINGS ────────────────────────────────────────┐
│ App                                                       │
│   Notes directory       /Users/you/.../zettelkasten/study│
│   Daily review limit    unlimited                         │
│   Auto-commit state     off                               │
│                                                           │
│ Card parsing                                              │
│   Decks pattern         tagged-line "tags:"               │
│   Deck delimiter        ":"                               │
│   Question pattern      wrapped "# Question" ↔ "# Answer" │
│   Answer pattern        wrapped "# Answer"  ↔ "----\n"    │
└───────────────────────────────────────────────────────────┘
Key Action
/, j/k Move between rows
Enter Edit the selected row (dispatches to a focused sub-editor)
Esc / Q Back to dashboard (parsing-config edits are written on exit)

Each row has its own editor:

  • Notes directory - text input with Tab-completion of directory paths.
  • Daily review limit - small text input (blank = unlimited).
  • Auto-commit state - toggles on/off in place.
  • Pattern rows - pattern editor: Tab toggles WrappedTagged-line, / moves between visible text fields, type to edit, Enter validates (rejects empty), Esc cancels.
  • Deck delimiter - small text input.

Persistence model

  • .vultan.ron (in your notes dir): card parsing config + per-card review state. Per-card path is relative to the notes dir, so the file is portable across machines. Also stores last_study_date, current_streak, longest_streak, per-card review_history (one date entry per scoring event), and per-card review_log (full snapshot of score + post-transform factor/interval). On every save vultan rotates a ring of three backups (.vultan.ron.bak.0 is most recent, .bak.2 is oldest), so a bad write or manual edit is recoverable.
  • ~/.config/vultan/config.ron (or $XDG_CONFIG_HOME/vultan/config.ron): user preferences (notes-dir path, daily review limit, auto-commit toggle).

.vultan.ron is safe (and recommended) to commit to your notes repo.

Configuration files

Most users never need to hand-edit either file - the Global settings screen covers everything in config.ron plus the card_parsing_config chunk of .vultan.ron. For the curious:

~/.config/vultan/config.ron:

(
    notes_dirpath: Some("/Users/you/Code/personal/zettelkasten/study"),
    daily_review_limit: Some(50),
    auto_commit_state: true,
)
  • daily_review_limit - when set, each session deals at most N cards (the most-overdue ones). None = unlimited.
  • auto_commit_state - when true and the notes dir is a git repo, vultan runs git add .vultan.ron && git commit -m "vultan: study session DATE" after each session. Only the state file is staged; your notes are never touched by vultan.

CLI shortcuts to inspect or set the saved config without opening the TUI:

vultan config show                       # print the current config
vultan config set-notes-dir <path>       # persist a new notes dir

--notes-dirpath on any subcommand is a one-shot override and does not update the saved default - use config set-notes-dir (or Global settings in-app) for that.

Anki interop

Vultan can both consume and produce Anki .apkg files for sharing decks with Anki users.

vultan import-anki --apkg deck.apkg --into ~/notes/anki-import
vultan export-anki --apkg out.apkg --deck-name math
vultan export-anki --apkg out.apkg --with-state    # preserve factor/ivl/due

Without --with-state (the default), exported cards land in Anki as new - the right behaviour for sharing. With --with-state, each card's factor/interval/due is mapped onto Anki's review queue so the recipient picks up where you left off.

Import handles modern Anki's zstd-compressed collection.anki21b format (Anki 2.1.50+) - it'll skip the "please update Anki" decoy card that modern Anki bundles for back-compat and pull the real cards out.

Shell completions

vultan completions zsh > ~/.zfunc/_vultan
# add `fpath=(~/.zfunc $fpath)` and `autoload -U compinit && compinit` to ~/.zshrc

Bash and fish are also supported (vultan completions bash / vultan completions fish).

Status

Work in progress, but daily-driver usable. The CLI surface and config schema may still shift; nothing on disk is encrypted or versioned beyond what your notes-dir VCS already provides, so back up your zettelkasten the way you already do.