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$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>.
Installing
From crates.io:
Or from a local clone of this repo:
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:
What is a composite number?
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.
Configuring card parsing
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.
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:
What is a composite number?
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
What is a composite number?
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
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:
Tabtoggles Wrapped ↔ Tagged-line,↑/↓moves between visible text fields, type to edit,Entervalidates (rejects empty),Esccancels. - Deck delimiter - small text input.
Persistence model
.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.
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 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:
--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.
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
# 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.