A terminal-based, Anki-compatible 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.
- 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$EDITORor 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 withE, 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
.apkgfiles (including modern zstd-compressed Anki 2.1.50+ exports); export your decks back to.apkgwith optional--with-stateto preserve factor/interval/due. - Safe persistence. A 3-generation ring-buffer backup of
.vultan.ronon 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>.
From crates.io:
cargo install vultanOr 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>.)
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 problemsand 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_tagand aclosing_tagbracketing the field. Good for multi-line / multi-paragraph fields.
There are two ways to change the parsing rules:
- In-app - open the dashboard, press
[G], navigate to one of the Card parsing rows, pressEnter. The pattern editor lets you toggle between Tagged-line and Wrapped withTaband edit the literal strings; saving validates and writes back to.vultan.ron. - Edit
.vultan.rondirectly - 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.
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.
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 cardsThe 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.
┌─ 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 |
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 |
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.
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.
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:
Tabtoggles Wrapped ↔ Tagged-line,↑/↓moves between visible text fields, type to edit,Entervalidates (rejects empty),Esccancels. - Deck delimiter - small text input.
.vultan.ron(in your notes dir): card parsing config + per-card review state. Per-cardpathis relative to the notes dir, so the file is portable across machines. Also storeslast_study_date,current_streak,longest_streak, per-cardreview_history(one date entry per scoring event), and per-cardreview_log(full snapshot of score + post-transform factor/interval). On every save vultan rotates a ring of three backups (.vultan.ron.bak.0is most recent,.bak.2is 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.
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 runsgit 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.
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/dueWithout --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.
vultan completions zsh > ~/.zfunc/_vultan
# add `fpath=(~/.zfunc $fpath)` and `autoload -U compinit && compinit` to ~/.zshrcBash and fish are also supported (vultan completions bash /
vultan completions fish).
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.
Vultan is an independent project. It is not affiliated with, endorsed by, or
sponsored by Ankitects Pty Ltd or the Anki project. "Anki" is a trademark of
Ankitects; the name appears in this README and in the import-anki /
export-anki subcommands solely to identify the .apkg file
format that vultan reads and writes for
interoperability. No Anki source code is bundled, linked, or derived;
vultan's own code is licensed MIT (see LICENSE), independent of Anki's
AGPL-3.0 license.