# 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](https://crates.io/crates/vultan):
```sh
cargo install vultan
```
Or from a local clone of this repo:
```sh
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:
```markdown
---
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:
| `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)**
```ron
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"),
)
```
```markdown
---
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.
```ron
card_parsing_config: (
decks_pattern: TaggedLine(tag: "tags:"),
deck_delimiter: ",",
question_pattern: TaggedLine(tag: "Q:"),
answer_pattern: TaggedLine(tag: "A:"),
)
```
```markdown
---
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**
```ron
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"),
)
```
```markdown
---
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
```sh
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│
└──────────────────────────────────────────────────────────┘
```
| `↑`/`↓`, `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:
| `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:
| `1` | Fail |
| `2` | Hard |
| `3` | Pass |
| `4` | Easy |
## Problems screen
Reachable with `P` from the dashboard.
| `↑`/`↓`, `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.
| `↑`/`↓`, `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" │
└───────────────────────────────────────────────────────────┘
```
| `↑`/`↓`, `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 *Wrapped* ↔ *Tagged-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`:
```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:
```sh
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.
```sh
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
```sh
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.