# Technical Documentation: ankitects/anki

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/ankitects/anki)

### Overview (`README.md`)

# Anki

[](https://github.com/ankitects/anki/actions/workflows/ci.yml)
[](https://dev-docs.ankiweb.net/en/latest/)
[](https://sonarcloud.io/summary/new_code?id=ankitects_anki)

This repo contains the source code for the computer version of
[Anki](https://apps.ankiweb.net).

## About

Anki is a spaced repetition program. Please see the [website](https://apps.ankiweb.net) to learn more.

## Getting Started

### Contributing

Want to contribute to Anki? Check out the [Contribution Guidelines](./docs/contributing.md).

For more information on building and developing, please see [Development](./docs/development.md).

#### Contributors

The following people have contributed to Anki: [CONTRIBUTORS](./CONTRIBUTORS)

### Anki Betas

If you'd like to try development builds of Anki but don't feel comfortable
building the code, please see [Anki betas](https://docs.ankiweb.net/releases/betas/intro).

## License

Anki's license: [LICENSE](./LICENSE)

---

### Overview (`cargo/README.md`)

This folder contains:

- a list of Rust crate licenses, which is checked/updated with ./ninja [check|fix]:minilints
- a nightly toolchain definition for formatting

---

### Overview (`docs-site/README.md`)

# Documentation Website

This directory contains [Mintlify](https://www.mintlify.com/) docs served at https://anki.mintlify.app/

## License

Unless stated otherwise, any documentation changes you submit will be licensed under the [CC BY-SA 4 license](https://creativecommons.org/licenses/by-sa/4.0/).

## Translations

To help translate the website to your language, please see
[Translating the Documentation](./translators/anki/manual.mdx).

---

### Overview (`ftl/README.md`)

Files related to Anki's translations.

Please see https://translating.ankiweb.net/anki/developers

---

### Overview (`proto/README.md`)

Protobuf files defining the interface the frontend and backend components use to talk to each other,
and how Anki stores some of the data inside its SQLite database. These files are used to generate Rust,
Python and TypeScript bindings.

---

### Overview (`pylib/README.md`)

# Anki

[](https://github.com/ankitects/anki/actions/workflows/ci.yml)

Core Python library for [Anki](https://apps.ankiweb.net), the spaced repetition flashcard program.

## About

[Anki](https://apps.ankiweb.net) is a spaced repetition program that helps you remember things efficiently. This package contains the Python layer of Anki's core: it wraps the Rust backend (`rslib`) and exposes the primary API used by the desktop app and add-ons alike.

It provides access to:

- **Collection** — open, read, and write an Anki `.anki2` database
- **Notes & Cards** — create, update, and query notes and cards
- **Decks & Models** — manage deck configurations and note types
- **Scheduler** — the FSRS/SM-2 scheduling algorithms
- **Media** — media file management and sync
- **Import / Export** — support for `.apkg`, `.colpkg`, and other formats
- **Sync** — synchronisation with AnkiWeb
- **Hooks** — event system for extending behaviour

## Installation

```bash
pip install anki
```

> **Note:** `anki` is the headless library. If you want the full desktop application, install [`aqt`](https://pypi.org/project/aqt/) instead, which depends on this package.

## Add-on development

If you are building an Anki add-on, this is the package that gives you access to the collection and scheduling internals. See the [Add-on Guide](https://docs.ankiweb.net/addons/intro) for full documentation.

## Contributing

Want to contribute? Check out the [Contribution Guidelines](https://github.com/ankitects/anki/blob/main/docs/contributing.md) and the [Development Guide](https://github.com/ankitects/anki/blob/main/docs/development.md).

## License

[AGPL-3.0-or-later](https://github.com/ankitects/anki/blob/main/LICENSE)

---

### Overview (`qt/README.md`)

# aqt — Anki Qt GUI

`aqt` is the Qt-based desktop interface for [Anki](https://apps.ankiweb.net), the spaced repetition flashcard program.

It provides all the visual components of the Anki desktop app: the deck browser, card editor, reviewer, browser, add-on manager, and more. Under the hood it uses PyQt6 and communicates with Anki's core logic via the [`anki`](https://pypi.org/project/anki/) package.

## Running

Once installed, Anki can be launched from the command line:

```bash
anki
```

## Add-on development

If you are building an Anki add-on, this is the package that exposes the GUI hooks and Qt widgets you need. See the [Add-on Guide](https://docs.ankiweb.net/addons/intro) for full documentation.

## Source code

<https://github.com/ankitects/anki>

---

### Overview (`ts/README.md`)

Anki's TypeScript and Sass dependencies. Some TS/JS code is also
stored separately in ../qt/aqt/data/web/.

To update all dependencies:

./update.sh

To add a new dev dependency, use something like:

./add.sh -D @rollup/plugin-alias

---

### Overview (`docs/docker/README.md`)

# Building and running Anki in Docker

This is an example Dockerfile contributed by an Anki user, which shows how Anki
can be both built and run from within a container. It works by streaming the GUI
over an X11 socket.

Building and running Anki within a container has the advantage of fully isolating
the build products and runtime dependencies from the rest of your system, but it is
a somewhat niche approach, with some downsides such as an inability to display natively
on Wayland, and a lack of integration with desktop icons/filetypes. But even if you
do not use this Dockerfile as-is, you may find it useful as a reference.

Anki's Linux CI runs on GitHub Actions. You can use, as references, the [`setup-anki`](../../.github/actions/setup-anki/action.yml)
to see the lists of packages CI installs, and [`ci.yml`](../../.github/workflows/ci.yml) to see the build steps that follow.

# Build the Docker image

For best results, enable BuildKit (`export DOCKER_BUILDKIT=1`).

When in this current directory, one can build the Docker image like this:

```bash
docker build --tag anki --file Dockerfile ../../
```

When this is done, run `docker image ls` to see that the image has been created.

If one wants to build from the project's root directory, use this command:

```bash
docker build --tag anki --file docs/docker/Dockerfile .
```

# Run the Docker image

Anki starts a graphical user interface, and this requires some extra setup on the user's
end. These instructions were tested on Linux (Debian 11) and will have to be adapted for
other operating systems.

To allow the Docker container to pull up a graphical user interface, we need to run the
following:

```bash
xhost +local:root
```

Once done using Anki, undo this with

```bash
xhost -local:root
```

Then, we will construct our `docker run` command:

```bash
docker run --rm -it \
    --name anki \
    --volume $HOME/.local/share:$HOME/.local/share:rw \
    --volume /etc/passwd:/etc/passwd:ro \
    --user $(id -u):$(id -g) \
    --volume /tmp/.X11-unix:/tmp/.X11-unix:rw \
    --env DISPLAY=$DISPLAY \
    anki
```

Here is a breakdown of some of the arguments:

- Mount the current user's `~/.local/share` directory onto the container. Anki saves things
  into this directory, and if we don't mount it, we will lose any changes once the
  container exits. We mount this as read-write (`rw`) because we want to make changes here.

  ```bash
  --volume $HOME/.local/share:$HOME/.local/share:rw
  ```

- Mount `/etc/passwd` so we can enter the container as ourselves. We mount this as
  read-only because we definitely do not want to modify this.

  ```bash
  --volume /etc/passwd:/etc/passwd:ro
  ```

- Enter the container with our user ID and group ID, so we stay as ourselves.

  ```bash
  --user $(id -u):$(id -g)
  ```

- Mount the X11 directory that allows us to open displays.

  ```bash
  --volume /tmp/.X11-unix:/tmp/.X11-unix:rw
  ```

- Pass the `DISPLAY` variable to the container, so it knows where to display graphics.

  ```bash
  --env DISPLAY=$DISPLAY
  ```

# Running Dockerized Anki easily from the command line

One can create a shell function that executes the `docker run` command. Then one can
simply run `anki` on the command line, and Anki will open in Docker. Make sure to change
the image name to whatever you used when building Anki.

```bash
anki() {
    docker run --rm -it \
        --name anki \
        --volume $HOME/.local/share:$HOME/.local/share:rw \
        --volume /etc/passwd:/etc/passwd:ro \
        --user $(id -u):$(id -g) \
        --volume /tmp/.X11-unix:/tmp/.X11-unix:rw \
        --env DISPLAY=$DISPLAY \
        anki "$@"
}
```

---

### Overview (`docs/syncserver/README.md`)

# Building and running Anki sync server in Docker

This is an example Dockerfile contributed by an Anki user, which shows how you can run a self-hosted sync server,
similar to what AnkiWeb.net offers.

Building and running the sync server within a container has the advantage of fully isolating
the build products and runtime dependencies from the rest of your system.

## Requirements

- [x] [Docker](https://docs.docker.com/get-started/)

| **Aspect**             | **Dockerfile**                                             | **Dockerfile.distroless**                                 |
| ---------------------- | ---------------------------------------------------------- | --------------------------------------------------------- |
| **Shell & Tools**      | ✅ Includes shell and tools                                | ❌ Minimal, no shell or tools                             |
| **Debugging**          | ✅ Easier debugging with shell and tools                   | ❌ Harder to debug due to minimal environment             |
| **Health Checks**      | ✅ Supports complex health checks                          | ❌ Health checks need to be simple or directly executable |
| **Image Size**         | ❌ Larger image size                                       | ✅ Smaller image size                                     |
| **Customization**      | ✅ Easier to customize with additional packages            | ❌ Limited customization options                          |
| **Attack Surface**     | ❌ Larger attack surface due to more installed packages    | ✅ Reduced attack surface                                 |
| **Libraries**          | ✅ More libraries available                                | ❌ Limited libraries                                      |
| **Start-up Time**      | ❌ Slower start-up time due to larger image size           | ✅ Faster start-up time                                   |
| **Tool Compatibility** | ✅ Compatible with more tools and libraries                | ❌ Compatibility limitations with certain tools           |
| **Maintenance**        | ❌ Higher maintenance due to larger image and dependencies | ✅ Lower maintenance with minimal base image              |
| **Custom uid/gid**     | ✅ It's possible to pass in PUID and PGID                  | ❌ PUID and PGID are not supported                        |

# Building image

To proceed with building, you must specify the Anki version you want, by replacing `<version>` with something like `24.11` and `<Dockerfile>` with the chosen Dockerfile (e.g., `Dockerfile` or `Dockerfile.distroless`)

The `--platform` option specifies which systems the image will be able to run on after it has been built. This is helpful in scenarios where you are building the image on one architecture (eg: an M series Mac) but running the image on another architecture (eg: x86 Linux server).

- `linux/amd64` is for x86 linux systems (Images built with this option will work on Windows as well through WSL2 which is included with Docker Desktop)
- `linux/arm64` is for ARM systems (eg: M series Macs)

On Linux systems, you will have to configure a custom builder which supports ARM builds first as shown below. You can then run the Mac build command in the next step.

```bash
# Configuring multiplatform builder and activating it
docker buildx create --name multiplatform-builder --driver docker-container --driver-opt default-load=true --bootstrap --use
```

On M-series Macs running Docker Desktop, you can build for both platforms using the default builder by running the command below:

```bash
# Execute this command from this directory
docker buildx build -f <Dockerfile> --platform linux/amd64,linux/arm64 --no-cache --build-arg ANKI_VERSION=<version> -t anki-sync-server .
```

If the build fails with `Resource Exhausted:.... cannot allocate memory`, go to Docker Desktop > Settings > Resources and increase the memory limit.

If you are unsure of what platform your system is, just run the command below without the platform option.

```bash
# Execute this command from this directory
docker buildx build -f <Dockerfile> --no-cache --build-arg ANKI_VERSION=<version> -t anki-sync-server .
```

# Run container

Once done with build, you can proceed with running this image with the following command:

```bash
# this will create anki server
docker run -d \
    -e "SYNC_USER1=admin:admin" \
    -p 8080:8080 \
    --mount type=volume,src=anki-sync-server-data,dst=/anki_data \
    --name anki-sync-server \
    anki-sync-server
```

If the image you are using was built with `Dockerfile` you can specify the
`PUID` and `PGID` env variables for the user and group id of the process that
will run the anki-sync-server process. This is valuable when you want the files
written and read from the `/anki_data` volume to belong to a particular
user/group e.g. to access it from the host or another container. Note the the
ids chosen for `PUID` and `PGID` must not already be in use inside the
container (1000 and above is fine). For example add `-e "PUID=1050"` and `-e
"PGID=1050"` to the above command.

If you want to have multiple Anki users that can sync their devices, you can
specify multiple `SYNC_USER` as follows:

```bash
# this will create anki server with multiple users
docker run -d \
    -e "SYNC_USER1=admin:admin" \
    -e "SYNC_USER2=admin2:admin2" \
    -p 8080:8080 \
    --mount type=volume,src=anki-sync-server-data,dst=/anki_data \
    --name anki-sync-server \
    anki-sync-server
```

Moreover, you can pass additional env vars mentioned
[here](https://docs.ankiweb.net/manual/sync-server). Note that `SYNC_BASE` and
`SYNC_PORT` will be ignored. In the first case for safety reasons, to avoid
accidentally placing data outside the volume and the second for simplicity
since the internal port of the container does not matter given that you can
change the external one.

# Upgrading

If your image was built after January 2025 then you can just build a new image
and start a new container with the same configuration as the previous
container. Everything should work as expected.

If the image you were running was built **before January 2025** then it did not
contain a volume, meaning all syncserver data was stored inside the container.
If you discard the container, for example because you want to build a new
container using an updated image, then your syncserver data will be lost.

The easiest way of working around this is by ensuring at least one of your
devices is fully in sync with your syncserver before upgrading the Docker
container. Then after upgrading the container when you try to sync your device
it will tell you that the server has no data. You will then be given the option
of uploading all local data from the device to syncserver.

---

### Overview (`qt/audio/README.md`)

Build scripts for bundled mpv/lame binaries.

## Credit

The macOS build scripts are adapted from [iina/deps-buildscripts](https://github.com/iina/deps-buildscripts).

---

### Overview (`qt/icons/README.md`)

Source files used to produce some of the svg/png files.

---

### Overview (`qt/installer/linux-template/README.md`)

This template is adapted from [briefcase-linux-system-template](https://github.com/beeware/briefcase-linux-system-template/) for building a zip with a standalone build of Python and custom install scripts.

---

### Overview (`ts/lib/generated/README.md`)

Files in this folder get combined with generated files in out/ts/lib/generated/

---

### Overview (`qt/installer/linux-template/{{ cookiecutter.format }}/{{ cookiecutter.app_name }}/README.md`)

# Installing Anki

## Running directly

To run without installing, change to this folder in a terminal, and run the
following command:

./anki

## Installing

To install system wide, run 'sudo ./install.sh'

To remove in the future, run 'sudo /usr/local/share/anki/uninstall.sh'.

## Audio

To play and record audio, mpv and lame must be installed. If mpv is not
installed or too old, Anki will try to fall back on using mplayer.

## Problems

If Anki fails to start, please run it from a terminal to see what errors it
outputs, and then post on our support site.

---

### Index (`docs/index.md`)

```{include} ../README.md
```

```{toctree}
---
maxdepth: 2
hidden:
---

development
build
contributing
dependabot
unit-testing
testing-coverage
sonarcloud
architecture
protobuf
```

## Auto-generated API References

```{toctree}
---
maxdepth: 1
hidden:
---

api-python
api-rust
```

---

### Api Aqt Modules (`docs/api-aqt-modules.md`)

# aqt

The pages below are generated from Python docstrings in `qt/aqt`.

```{toctree}
:maxdepth: 2
:glob:

autoapi/aqt/*/index
autoapi/aqt/*/*/index
autoapi/aqt/*/*/*/index
```

---

### Api Python Modules (`docs/api-python-modules.md`)

# anki

The pages below are generated from Python docstrings in `pylib/anki`.

```{toctree}
:maxdepth: 2
:glob:

autoapi/anki/*/index
autoapi/anki/*/*/index
```

---

### Api Python (`docs/api-python.md`)

# Python API

```{toctree}
:maxdepth: 2

api-python-modules
api-aqt-modules
```

---

### Api Rust (`docs/api-rust.md`)

# Rust API

Anki's Rust API documentation is generated from doc comments in the `rslib/`
crates using `cargo doc`.

To browse the docs locally:

```
cargo doc --open
```

The main crates are:

- **anki** (`rslib/`) - Core library
- **anki_io** (`rslib/io/`) - I/O utilities

---

### Architecture (`docs/architecture.md`)

# Anki Architecture



Very brief notes for now.

## Backend/GUI

At the highest level, Anki is logically separated into two parts.

A neat visualization of the file layout is available here:
[https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=ankitects%2Fanki](https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=ankitects%2Fanki)
(or go to [https://githubnext.com/projects/repo-visualization#explore-for-yourself](https://githubnext.com/projects/repo-visualization#explore-for-yourself) and enter `ankitects/anki`).

### Library (rslib & pylib)

The Python library (pylib) exports "backend" methods - opening collections,
fetching and answering cards, and so on. It is used by Anki’s GUI, and can also
be included in command line programs to access Anki decks without the GUI.

The library is accessible in Python with "import anki". Its code lives in
the `pylib/anki/` folder.

These days, the majority of backend logic lives in a Rust library (rslib, located in `rslib/`). Calls to pylib proxy requests to rslib, and return the results.

pylib contains a private Python module called rsbridge (`pylib/rsbridge/`) that wraps the Rust code, making it accessible in Python.

### GUI (aqt & ts)

Anki's _GUI_ is a mix of Qt (via the PyQt Python bindings for Qt), and
TypeScript/HTML/CSS. The Qt code lives in `qt/aqt/`, and is importable in Python
with "import aqt". The web code is split between `qt/aqt/data/web/` and `ts/`,
with the majority of new code being placed in the latter, and copied into the
former at build time.

## Protobuf

Anki uses Protocol Buffers to define backend methods, and the storage format of
some items in a collection file. The definitions live in `proto/anki/`.

The Python/Rust bridge uses them to pass data back and forth, and some of the
TypeScript code also makes use of them, allowing data to be communicated in a
type-safe manner between the different languages.

At the moment, the protobuf is not considered public API. Some pylib methods
expose a protobuf object directly to callers, but when they do so, they use a
type alias, so callers outside pylib should never need to import a generated
\_pb2.py file.

---

### Build (`docs/build.md`)

# The build system



## Basic use

Basic use is described in [development.md](https://anki.mintlify.app/developers/development).

## Architecture

The build/ folder is made up of 4 packages:

- build/configure defines the actions and inputs/outputs of the build graph -
  this is where you add new build steps or modify existing ones. The defined
  actions are converted at build time to a build.ninja file that Ninja executes.
- build/ninja_gen is a library for writing a build.ninja file, and includes
  various rules like "build a Rust crate" or "run a command".
- build/archives is a helper to download/checksum/extract a dependency as part
  of the build process.
- build/runner serves a number of purposes:
    - it's the entrypoint to the build process, taking care of generating
      the build file and then invoking Ninja
    - it wraps executable invocations in the build file, swallowing their output
      if they exit successfully
    - it provides a few helpers for multi-step processes that can't be easily
      described in a cross-platform manner thanks to differences on Windows.

## Tracing build problems

If you run into trouble with the build process:

- You can see the executed commands with e.g. `./ninja pylib -v`
- You can see the output of successful commands by defining N2_OUTPUT_SUCCESS=1
- You can see what's triggering a rebuild of a target with e.g.
  `./ninja qt/anki -d explain`.
- You can browse the build graph via e.g. `./ninja -- -t browse wheels`
- You can profile build performance with
  https://discourse.cmake.org/t/profiling-build-performance/2443/3.

## Packaging considerations

See [this page](https://anki.mintlify.app/developers/linux).

---

### Contributing (`docs/contributing.md`)

# Contributing Code



For info on contributing things other than code, such as translations, decks
and add-ons, please see [the contribution guide](https://anki.mintlify.app/manual/contrib).

## Help wanted

If you'd like to contribute but don't know what to work on, please take a look
at the [issues tab](https://github.com/ankitects/anki/issues) of the Anki repo
on GitHub.

## Larger changes

Before starting work on larger changes, especially ones that aren't listed on the
issue tracker, please reach out on the forums before you begin work, so we can let
you know whether they're likely to be accepted or not. When you spent a bunch of time
on a PR that ends up getting rejected, it's no fun for either you or us.

## Consider an Add-on First

Before submitting a PR for a new feature, please consider whether it could be
implemented as an add-on instead. We aim to keep the core Anki codebase lean
and maintainable. Many great ideas are better served as add-ons, where they
can iterate faster and serve specific user needs without affecting all users.

See the [Add-on API documentation](https://addon-docs.ankiweb.net/) for
guidance on building add-ons.

## Pull Request Description

When opening a pull request, fill in the provided template
(`.github/pull_request_template.md`). It covers the required fields: linked
issue, summary/motivation, how to test, and UI evidence for visual changes.

## Linked Issues

Every pull request, except hotfixes and dependency updates, must be linked
to an existing open issue. If no issue exists, please open one to describe
the problem before submitting a PR. This helps us ensure we're solving the
right problems and prevents wasted effort on both sides.

PRs without a linked issue may be automatically closed after a short period.

## Dependency updates

Dependabot PRs do not need a linked issue. Maintainers should follow
[Dependabot updates](https://anki.mintlify.app/developers/dependabot) when reviewing or landing them.

## Refactoring

Please avoid PRs that focus on refactoring. Every PR has a cost to review, and a chance
of introducing accidental regressions, and often these costs are not worth it for
slightly more elegant code.

That's not to say there's no value in refactoring. But such changes are usually better done
in a PR that happens to be working in the same area - for example, making small changes
to the code as part of fixing a bug, or a larger refactor when introducing a new feature.

## Type hints

Most of Anki's Python code now has type hints, which improve code completion,
and make it easier to discover errors during development. When adding new
code, please make sure you add type hints as well, or the tests will fail.

Qt's stubs are not perfect, so you may sometimes need to use cast(), or silence
a type error. When connecting signals, there's a qconnect() helper in aqt.utils
that can be used to work around the type warnings without obscuring other errors
such as a mistyped variable.

In cases where you have two modules that reference each other, you can fix the
import cycle by using fully qualified names in the types, and enabling
annotations. For example, instead of

```
from aqt.browser import Browser

def myfunc(b: Browser) -> None:
  pass
```

use the following instead:

```
from __future__ import annotations

import aqt

def myfunc(b: aqt.browser.Browser) -> None:
  pass
```

## Hooks

If you're writing an add-on and would like to extend a function that doesn't
currently have a hook, a pull request that adds the required hooks would be
welcome. If you could mention your use case in the pull request, that would be
appreciated.

The hooks try to follow one of two formats:

[subject] [verb] - eg, note_type_added, card_will_render

[module] [verb] [subject] - eg, browser_did_change_row, editor_did_update_tags

The qt code tends to use the second form, as the hooks tend to focus on
particular screens. The pylib code tends to use the first form, as the focus
is usually subjects like cards, notes, etc.

Using "did change" instead of the past tense "changed" can seem awkward, but
makes it consistent with "will", and is similar to the naming style used in
iOS's libraries.

In most cases, hooks are better added in the GUI code than in pylib.

The hook code is automatically generated using the definitions in
pylib/tools/genhooks.py and qt/tools/genhooks_gui.py. Adding a new definition
in one of those files will update the generated files.

If you want to change an existing hook to, for example, receive an additional
argument, you must leave the existing hook unchanged to preserve backwards
compatibility. Create a new definition for your hook with a similar name and
include the properties `replaces="name_of_old_hook"` and
`replaced_hook_args=["..."]` in the definition of the new hook. If the old hook
has a legacy hook, you must not add the legacy hook to the definition of the
new hook.

## Translations

For information on adding new translatable strings to Anki, please see
[the translation guide](https://anki.mintlify.app/translators/anki/developers).

For information on translating the documentation, please see
[translating the manual](https://anki.mintlify.app/translators/anki/manual).

## Tests Must Pass

Please make sure 'ninja check' completes successfully before submitting code.

[pre-commit](https://pre-commit.com/) is used to run that check from a Git hook.
It is configured in `.pre-commit-config.yaml` at the repository root. After
installing the dev dependencies (for example `uv sync --group dev`), run one of:

```
uv run pre-commit install --hook-type pre-push
```

```
python3 -m pre_commit install --hook-type pre-push
```

(`pre-commit` alone only works if that executable is on your `PATH`, for example
after `pip install --user pre-commit` or with your virtual environment activated.)

The bundled hook runs `./ninja check` on **pre-push** (not on every commit),
because the full check suite can take a long time. You can still run
`./ninja check` manually at any time.

You may need to ensure your usual shell `PATH` is visible to the hook (for
example if tools such as `ninja` are installed outside standard locations),
because pre-commit does not use a login shell and a different path can cause
extra rebuilds.

New or changed behavior must be covered by tests. Exceptions where tests are not
required: version bumps, documentation-only changes, translation updates, dependency
updates, and CI/build housekeeping (chore).

## Code Style

Please use standard Python snake_case variable names and functions in newly
introduced code. Because add-ons often rely on existing function names, if
renaming an existing function, please add a legacy alias to the old function.

## Do One Thing

A patch or pull request should be the minimum necessary to address one issue.
Please don't make a pull request for a bunch of unrelated changes, as they are
difficult to review and will be rejected - split them up into separate
requests instead.

## AI-Assisted Contributions

Using AI tools to help write or review code is permitted. However, you must
understand every change you submit. If you cannot explain what your changes
do and how they interact with the rest of the system, your PR will be closed.

Please review AI-generated code carefully before submitting. PRs that appear
to have been submitted without human review — e.g., irrelevant code, duplicate
logic, or comments that don't match the implementation — may be closed without
further discussion.

## License

Please add yourself to the [CONTRIBUTORS](https://github.com/ankitects/anki/blob/main/CONTRIBUTORS) file in your first pull request.

---

### Dependabot (`docs/dependabot.md`)

# Dependabot updates



Config lives in [`.github/dependabot.yml`](https://github.com/ankitects/anki/blob/main/.github/dependabot.yml).
This page is for maintainers: how PRs are grouped, which ones to merge,
and how to clear Rust advisories that Dependabot cannot fix on its own.

## Goals

Version updates were accumulating faster than anyone reviewed them. Our solution is to fix grouping and slow the schedule, not turn updates off.

Security updates stay **individual**. Each advisory needs its own review (false positives are common, so we need to know which CVE actually matters). GitHub also does not apply `open-pull-requests-limit` to security PRs.

## What gets opened

Four ecosystems: Cargo, npm (Yarn), Python (`uv`), GitHub Actions.

| Kind           | When                                                           | Shape                                          | What to do                                                                                       |
| -------------- | -------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Security       | As soon as GitHub publishes an advisory. Ignores the schedule. | One PR per advisory                            | Review and land promptly, or close if it does not apply                                          |
| Minor / patch  | Quarterly                                                      | One grouped PR per ecosystem (`*-minor-patch`) | Merge if CI is green                                                                             |
| Major          | Quarterly, after a 30-day cooldown                             | One grouped PR per ecosystem (`*-major`)       | **Do not merge.** Treat as a notification. If a bump is wanted, open a human PR for that package |
| GitHub Actions | Quarterly                                                      | One grouped PR (`actions`)                     | Merge if CI is green                                                                             |

`open-pull-requests-limit` is 2 for Cargo / npm / Python (one slot for minor/patch, one for major) and 1 for Actions. That limit applies only to **version** updates. Security PRs are unlimited.

Cooldown is 7 days for ordinary version bumps (a short supply-chain window) and 30 days for majors (`semver-major-days`). Security updates skip cooldown.

Dependabot PRs are exempt from the linked-issue requirement.

## Reviewing a PR

1. Read the title / group name and put it in the table above;
2. CI must be green. Dependency PRs do not need new tests, but `format`, `cargo deny`, and the existing suites still run;
3. **npm:** if CI fails with `YN0028` / `yarn install --immutable`, Dependabot updated `package.json` and left `yarn.lock` stale. Check out the branch, run Yarn so the lockfile matches, and push;
4. Close older individual version PRs that a newer grouped PR already covers. Dependabot often does this itself when it opens a replacement.

### Security PRs

Land these first. Confirm the advisory actually affects this repo: Dependabot matches on version, not on whether we call the vulnerable API.

If the bump is a major, or CI fails, do not force the Dependabot PR through. Open a normal PR with whatever code or lockfile changes are required.

If we cannot bump yet, say so in the PR and follow the Rust ignore path below when `cargo-deny` is the failing check.

### Minor / patch grouped PRs

Low risk. Merge when CI is green. If one crate in the group breaks the build, do not merge the bundle, we should split or skip that crate.

### Major grouped PRs

Majors often need code changes. The grouped PR exists so we are not flooded with one PR per package. _It is not meant to be merged as-is_. Pick the package we actually want, open a dedicated PR, and leave the rest.

### GitHub Actions

Actions are pinned by SHA. Dependabot **security alerts are not generated for SHA-pinned actions** but Dependabot still opens the quarterly grouped version PR and bumps those SHAs to the latest release. So there is no immediate security PR for a vulnerable action. That is an accepted trade-off: SHA pins stop tag-rewrite supply-chain attacks, at the cost of losing action alerts.

## Rust advisories (`cargo-deny`)

CI runs `cargo deny check` on pushes and on PRs that touch `Cargo.lock`, `Cargo.toml`, or `.deny.toml`. Failures often look like Dependabot work but are not something Dependabot can finish, especially when the crate sits behind a git dependency.

Worked example: [RUSTSEC-2026-0258](https://github.com/ankitects/anki/issues/5364) needed a `reqwest` bump inside the [ankitects/linkcheck](https://github.com/ankitects/linkcheck) fork, not a one-line change in this repo.

1. Open the `RUSTSEC-…` advisory and note the crate and patched versions.
2. See whether Dependabot already opened a security PR that bumps a crate we depend on directly. If CI is green, merge it.
3. If the crate is **transitive through a git dep** (`linkcheck`, `percent-encoding-iri`, …):
    - Land the bump in the fork.
    - Point `Cargo.toml` at the new `rev`.
    - Run `cargo update -p <crate>` (and any other crates the advisory names) so `Cargo.lock` moves.
4. Run `cargo deny check` locally before pushing.
5. Only if a bump is not feasible, add the id to `.deny.toml` `[advisories] ignore` with a comment stating why and what the alternative is. Existing ignores follow that pattern. Do not silence an advisory without a comment.

Dependabot cannot update git `rev` pins. Those bumps are always manual.

## Changing the config

- Do not group security updates;
- Do not put majors in the minor/patch group;
- `open-pull-requests-limit` cannot cap security PRs;
- Cooldown keys are `default-days` and `semver-major-days`.

---

### Development (`docs/development.md`)

# Anki development



## Packaged betas

For non-developers who want to try beta versions, the easiest way is to use a
packaged version - please see:

https://betas.ankiweb.net/

## Pre-built Python wheels

Pre-built Python packages are available on PyPI. They are useful if you wish to:

- Run Anki from a local Python installation without building it yourself
- Get code completion when developing add-ons
- Make command line scripts that modify .anki2 files via Anki's Python libraries

You will need the 64 bit version of Python 3.10 or later installed. You can install
Python from python.org or from your distro.

For further instructions, please see https://betas.ankiweb.net/#via-pypipip. Note that
in the provided commands, `--pre` tells pip to fetch alpha/beta versions. If you remove
`--pre`, it will download the latest stable version instead.

## Building from source

Clone the git repo into a folder of your choosing. The folder path must not
contain spaces, and should not be too long if you are on Windows.

On all platforms, you will need to install:

- Rustup (https://rustup.rs/). The Rust version pinned in rust-toolchain.toml
  will be automatically downloaded if not yet installed. If removing that file
  to use a distro-provided Rust, newer Rust versions will typically work for
  building but may fail tests; older Rust versions may not work at all.
- N2 or Ninja. N2 gives better status output. You can install it with `tools/install-n2`,
  or `bash tools\install-n2` on Windows. If you want to use Ninja, it can be downloaded
  from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
  placed on your path, or from your distro/homebrew if it's 1.10+.
    - On Windows, if you have WSL installed, it may conflict with MSYS2 bash. If you are getting an error, try running `C:\msys64\usr\bin\bash.exe tools/install-n2` instead.
- (Optional) [just](https://just.systems/man/en/packages.html) command runner.
  Install with `brew install just` or `uv tool install rust-just`.
  We are experimenting with `just` as the official tool for running
  Anki-specific commands, and it will likely become the source of truth
  in the future.

Platform-specific requirements:

- [Windows](https://anki.mintlify.app/developers/windows)
- [Mac](https://anki.mintlify.app/developers/mac)
- [Linux](https://anki.mintlify.app/developers/linux)

## Running Anki during development

From the top level of Anki's source folder:

```
./run
```

(`.\run` on Windows)

This will build Anki and run it in place.

The first build will take a while, as it downloads and builds a bunch of
dependencies. When the build is complete, Anki will automatically start.

If Anki fails to start, you may need to install [extra libraries](https://anki.mintlify.app/manual/platform/linux/missing-libraries).

## Running tests/checks

To run all tests at once, from the top-level folder:

```
./ninja check
```

(`tools\ninja check` on Windows).

You can also run specific checks. For example, if you see during the checks
that `check:svelte:editor` is failing, you can use `./ninja check:svelte:editor`
to re-run that check, or `./ninja check:svelte` to re-run all Svelte checks.

## Fixing formatting

When formatting issues are reported, they can be fixed with

```
./ninja format
```

## Fixing ruff/eslint/copyright header issues

```
./ninja fix
```

## Fixing clippy issues

```
cargo clippy --fix
```

## Excluding your own untracked files from formatting and checks

If you want to add files or folders to the project tree that should be excluded from version tracking and not be matched by formatters and checks, place them in an `extra` folder and they will automatically be ignored.

## Optimized builds

The `./run` command will create a non-optimized build by default. This is faster
to compile, but will mean Anki will run slower.

To run Anki in optimized mode, use:

```
./tools/runopt
```

Or set RELEASE=1 or RELEASE=2. The latter will further optimize the output, but make
the build much slower.

## Building redistributable wheels

The `./run` method described in the platform-specific instructions is a shortcut
for starting Anki directly from the build folder. For regular study, it's recommended
you build Python wheels and then install them into your own python venv. This is also
a good idea if you wish to install extra tools from PyPi that Anki's build process
does not use.

To build wheels on Mac/Linux:

```
./tools/build
```

(on Windows, `.\tools\build.bat`)

The generated wheels are in out/wheels. You can then install them by copying the paths into a pip install command.
Follow the steps [on the beta site](https://betas.ankiweb.net/#via-pypipip), but replace the
`pip install --upgrade --pre aqt` line with something like:

```
/my/pyenv/bin/pip install --upgrade out/wheels/*.whl
```

(On Windows you'll need to list out the filenames manually instead of using a wildcard).

## Cleaning up build files

Apart from submodule checkouts, most build files go into the `out/` folder (and
`node_modules` on Windows). You can delete that folder for a clean build, or
to free space.

Cargo, yarn and pip all cache downloads of dependencies in a shared cache that
other builds on your system may use as well. If you wish to clear up those caches,
they can be found in `~/.rustup`, `~/.cargo` and `~/.cache/{yarn,pip}`. On
Windows, Yarn cache can be found in `%LOCALAPPDATA%\Yarn`.

If you invoke Rust outside of the build scripts (eg by running cargo, or
with Rust Analyzer), output files will go into `target/` unless you have
overridden the default output location.

## Storage and Performance

Most editors use rust-analyzer for Rust support, which runs `cargo check` in the
background whenever you save a file, so that problems are surfaced before you try to
build. This is useful, but on lower-end machines, it can noticeably increase disk
usage and CPU load. If that's a problem for you, consider disabling "check on save"
(e.g. set `rust-analyzer.checkOnSave` to `false` in VS Code) as a trade-off — you'll
lose real-time error checking, but rust-analyzer can still be used for code completion,
go-to-definition, etc.

If you chose to disable "check on save" and don't run `cargo` commands manually, you
may delete the `target/` folder (if any) as that's no longer used.

## IDEs

Please see [this separate page](https://anki.mintlify.app/developers/editing) for setting up an editor/IDE.

## Making changes to the build

See [this page](https://anki.mintlify.app/developers/build)

## Generating documentation

Build and view the documentation site:

```
just docs
```

For Rust API docs:

```
just docs-rust
```

## Environmental Variables

If ANKIDEV is set before starting Anki, some extra log messages will be printed on stdout,
and automatic backups will be disabled - so please don't use this except on a test profile.
It is automatically enabled when using ./run.

If TRACESQL is set, all SQL statements will be printed as they are executed.

If LOGTERM is set before starting Anki, warnings and error messages that are normally placed
in the collection2.log file will also be printed on stdout.

If ANKI_PROFILE_CODE is set, Python profiling data will be written on exit.

## Installer

Run `tools/build-installer` to build the installer.

Depending on your operating system, this produces a file under `out/installer/dist`:

- An MSI installer on Windows.
- A .dmg file on macOS.
- A tarball on Linux.

### Issues During Building

If you are experiencing issues building the installer, make sure that all dependencies
are installed. See [Building from source](https://anki.mintlify.app/developers/development#building-from-source) for more info.

## Releasing

See [Releasing](https://anki.mintlify.app/developers/releasing).

# Mixing development and study

You may wish to create a separate profile with File>Switch Profile for use
during development. You can pass the arguments "-p [profile name]" when starting
Anki to load a specific profile.

If you're using PyCharm:

- right click on the "run" file in the root of the PyCharm Anki folder
- click "Edit 'run'..." - in Script options and enter:
  "-p [dev profile name]" without the quotes
- click "Ok"

---

### E2e Testing (`docs/e2e-testing.md`)

# End-to-End Testing with Playwright



Playwright drives a real headless Anki instance via its mediasrv HTTP API.
Tests live in `ts/tests/e2e/` and are entirely separate from the Vitest unit tests.

## Prerequisites

Build Anki at least once before running e2e tests:

```shell
just build
```

That's it. `just test-e2e` automatically installs Playwright's Chromium browser
into `out/playwright-browsers/` on the first run (idempotent on subsequent runs).

## Running tests

### Managed mode (CI-style)

Playwright starts and stops a throwaway Anki instance automatically:

```shell
just test-e2e
```

The first run can be slow (~60 s) because Anki must fully initialise before tests start.

### Reuse-server mode (recommended for development)

Start Anki once in a separate terminal, then reuse it across multiple test runs:

```shell
# Terminal 1 — keep running
./run

# Terminal 2 — fast iteration
ANKI_E2E_REUSE_SERVER=1 just test-e2e
```

### Interactive UI mode

Open Playwright's browser UI to inspect each test step with snapshots:

```shell
ANKI_E2E_REUSE_SERVER=1 just test-e2e --ui
```

## Writing tests

Add test files to `ts/tests/e2e/` with the `.test.ts` suffix and import from
`./fixtures` instead of directly from `@playwright/test`:

```typescript
import { expect, test } from "./fixtures";

test("my feature works", async ({ page }) => {
    await page.goto("/some-anki-page");
    await expect(page.locator("#some-element")).toBeVisible();
});
```

`fixtures.ts` re-exports `expect` and a pre-configured `test` object. Add
shared fixtures there as new features require them.

## Calling the Anki API from tests

Anki's `/_anki/` endpoints accept and return protobuf-encoded binary payloads
(`Content-Type: application/binary`). Use `page.request.post` with
`Buffer.from(protoMsg.toBinary())` and decode the response with the matching
generated type from `ts/lib/generated/`.

## Accessing Anki pages

Anki's mediasrv serves the following pages over HTTP (non-exhaustive):

| URL pattern              | Description                   |
| ------------------------ | ----------------------------- |
| `/graphs`                | Statistics graphs (SvelteKit) |
| `/deck-options/[deckId]` | Deck options (SvelteKit)      |
| `/congrats`              | Post-study screen (SvelteKit) |
| `/card-info/[cardId]`    | Card info (SvelteKit)         |
| `/editor/?mode=[mode]`   | Editor (SvelteKit)            |
| `/congrats`              | Congrats page (SvelteKit)     |
| `/favicon.ico`           | Mediasrv liveness probe       |

## CI

The e2e tests run as part of the `check-linux` job in `.github/workflows/ci.yml`,
after the regular build and test steps. Screenshots and traces from failed runs
are uploaded as artifacts and kept for 7 days.

## See also

- [Writing Unit Tests for Anki](https://anki.mintlify.app/developers/unit-testing)

---

### Editing (`docs/editing.md`)

# Editing/IDEs



Visual Studio Code is recommended, since it provides decent support for all the languages
Anki uses. To set up the recommended workspace settings for VS Code, please see below.

For editing Python, PyCharm/IntelliJ's type checking/completion is a bit nicer than
VS Code, but VS Code has improved considerably in a short span of time.

There are a few steps you'll want to take before you start using an IDE.

## Initial Setup

### Python Environment

For code completion of external Python modules, you can use the venv that is
generated as part of the build process. After building Anki, the venv will be in
`out/pyenv`. In VS Code, use ctrl/cmd+shift+p, then 'python: select
interpreter'.

### Rust

You'll need Rust to be installed, which is required as part of the build process.

### Build First

Code completion partly depends on files that are generated as part of the
regular build process, so for things to work correctly, use './run' or
'tools/build' prior to using code completion.

## Visual Studio Code

### Setting up Recommended Workspace Settings

To start off with some default workspace settings that are optimized for Anki
development, please head to the project root and then run:

```
mkdir .vscode && cd .vscode
ln -sf ../.vscode.dist/* .
```

### Installing Recommended Extensions

Once the workspace settings are set up, open the root of the repo in VS Code to
see and install a number of recommended extensions.

## PyCharm/IntelliJ

### Setting up Python environment

To make PyCharm recognize `anki` and `aqt` imports, you need to add source paths to _Settings > Project Structure_.
You can copy the provided .idea.dist directory to set up the paths automatically:

```
mkdir .idea && cd .idea
ln -sf ../.idea.dist/* .
```

You also need to add a new Python interpreter under _Settings > Python > Interpreter_ pointing to the Python executable under `out/pyenv` (available after building Anki).

---

### Language Bridge (`docs/language_bridge.md`)

# Language Bridge



Anki's codebase uses three layers.

1. The web frontend, created in Svelte and typescript,
2. The Python layer and
3. The core Rust layer.

Each layer can can makes RPC (Remote Procedure Call) to the layers below it. While it should be avoided, Python can also invoke Typescript functions. The Rust layers never make calls to the other layers. Note that it can make RPC to AnkiWeb and other servers, which is out of scope of this document.

In this document we'll provide examples of bridge between languages, explaining:

- where the RPC is declared,
- where it is called (with the appropriate imports) and
- where it is implemented.

Imitating those examples should allow you to make call and create new RPCs.

## Declaring RPCs

Let's consider the method `NewDeck` of `DecksServices`. It's declared in [decks.proto](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/proto/anki/decks.proto#L14) as `rpc NewDeck(generic.Empty) returns (Deck);`. This means this methods takes no argument (technically, an argument containing no information), and returns a [`Deck`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/proto/anki/decks.proto#L54).

Read [protobuf](https://anki.mintlify.app/developers/protobuf) to learn more about how those input and output types are defined.

If the RPC implementation is in Python, it should be declared in the service [frontend.proto](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/proto/anki/frontend.proto#L24C3-L24C66)'s `FrontendService`. RPCs declared in any other services are implemented in Rust.

## Making a Remote Procedure Call

In this section we'll consider how to make Remote Procedure Call (RPC) from languages used in Anki. Languages used for AnkiDroid and AnkiMobile are out of scope of this document.

### Making a RPC from Python

Python can invoke the `NewDeck` method with [`col._backend.new_deck()`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/pylib/anki/decks.py#L168). This python method takes no argument and returns a `Deck` value.

However, most Python code should not call this method directly. Instead it should call [`col.decks.new_deck()`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/pylib/anki/decks.py#L166). Generally speaking, all back-end functions called from Python should be called through a helper method defined in `pylib/anki/`. The `_backend` part is an implementation detail that most callers should ignore. This is especially important because add-ons should expect a relatively stable API independent of the implementation details of the RPC.

### Invoking method from TypeScript

Let's consider the method [`rpc GetCsvMetadata(CsvMetadataRequest) returns (CsvMetadata);`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/proto/anki/import_export.proto#L20) from `ImportExportService`..

It's used in the TypeScript class [`ImportCsvState`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/ts/routes/import-csv/lib.ts#L102), as an asynchronous function. It's argument is a single javascript object, whose keys are as in [`CsvMetadataRequest`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/proto/anki/import_export.proto#L138) and it returns a `CsvMetadata`.

The method was imported with `import { getCsvMetadata } from "@generated/backend";` and the types were imported with `import type { CsvMetadata } from "@generated/anki/import_export_pb";`. Note that it was not necessary to import the input type given that it's simply an untyped javascript object.

## Implementation

Let's now look at implementations of those RPCs.

### Implementation in Rust

The method NewDeck is implemented in Rust's [DecksService](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/rslib/src/decks/service.rs#L21) as `fn new_deck(&mut self) -> error::Result<anki_proto::decks::Deck>`. It should be noted that the method name was changed from Pascal case to snake case, and the rps's argument of type `generic.Empty` is ignored.

### Implementation in Python

Let's consider the implementation of the method [DeckOptionsRequireClose](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/qt/aqt/mediasrv.py#L578). It's defined as `def deck_options_require_close() -> bytes:`. In this case, there should be a returned value. However, it'll be ignored, so returning `b""` is perfectly fine.

Note that the incoming HTTP request is not processed on the main thread. In order to do any work with the GUI, we should call `aqt.mw.taskman.run_on_main`.

## Invoking a TypeScript method from Python

This case should be avoided if possible, as we generally should avoid
calls to the upper layer. Contrary to the previous cases, we don't use
protobuf.

### Calling a TS function.

Let's take as Example [`export function getTypedAnswer(): string | null`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/ts/reviewer/index.ts#L35). It's an exported function, and its return type can be encoded in JSON.

It's called in the Reviewer class through [`self.web.evalWithCallback("getTypedAnswer();", self._onTypedAnswer)`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/qt/aqt/reviewer.py#L785). The result is then sent to [`_onTypedAnswer`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/qt/aqt/reviewer.py#L787).

If no return value is needed, `web.eval` would have been sufficient.

### Calling a Svelte method

Let's now consider the case where the method we want to call is implemented in a Svelte library. Let's take as example [`deckOptionsPendingChanges`](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/ts/routes/deck-options/%5BdeckId%5D/%2Bpage.svelte#L17). We define it with:

```js
globalThis.anki || = {};
globalThis.anki.methodName = async (): Promise<void>=>{body}
```

Note that if the function is asynchronous, you can't directly send the
result to a callback. Instead your function will have to call a post
method that will be sent to Python or Rust.

This method is called in [deckoptions.py](https://github.com/ankitects/anki/blob/acaeee91fa853e4a7a78dcddbb832d009ec3529a/qt/aqt/deckoptions.py#L68) with `self.web.eval("anki.deckOptionsPendingChanges();"`.

---

### Linux (`docs/linux.md`)

# Linux-specific notes



## Requirements

These instructions are written for Debian/Ubuntu; adjust for your distribution.
Some extra notes have been provided by a forum member, though some of the things
mentioned there no longer apply:
https://forums.ankiweb.net/t/guide-how-to-build-and-run-anki-from-source-with-xubuntu-20-04/12865

You can see a full list of buildtime and runtime requirements by looking at the `apt-get install` step in the [`setup-anki` composite action](https://github.com/ankitects/anki/blob/main/.github/actions/setup-anki/action.yml),
used by the workflows under [`.github/workflows/`](https://github.com/ankitects/anki/tree/main/.github/workflows).

**Ensure some basic tools are installed**:

```
$ sudo apt install bash grep findutils curl gcc gcc-12 g++ make git rsync
```

- The 'find' utility is 'findutils' on Debian.

## Missing Libraries

If you get errors during build or startup, try starting with

QT_DEBUG_PLUGINS=1 ./run

It will likely complain about missing libraries, which you can install with
your package manager. Some of the libraries that might be required on Debian
for example:

```
sudo apt install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
  libxcb-randr0 libxcb-render-util0 libxkbfile1
```

The libraries that might be required on Arch Linux:

```
sudo pacman -S nss libxkbfile
```

On some distros such as Fedora, you may need to install the
`libxcrypt-compat` package if you get an error like this:

```
error while loading shared libraries: libcrypt.so.1: cannot open shared object file: No such file or directory
```

## Audio

To play and record audio during development, install mpv and lame.

## Glibc and Qt

Anki requires a recent glibc.

If you are using a distro that uses musl, Anki will not work.

You can use your system's Qt libraries if they are Qt 6.2 or later, if
you wish. After installing the system libraries (eg:
'sudo apt install python3-pyqt6.qt\{quick,webengine\} python3-venv pyqt6-dev-tools'),
find the place they are installed (eg '/usr/lib/python3/dist-packages'). On modern Ubuntu, you'll
also need 'sudo apt remove python3-protobuf'. Then before running any commands like './run', tell Anki where
the packages can be found:

```
export PYTHONPATH=/usr/lib/python3/dist-packages
export PYTHON_BINARY=/usr/bin/python3
```

## Packaging considerations

Python, node and protoc are downloaded as part of the build. You can optionally define
PYTHON_BINARY, NODE_BINARY, YARN_BINARY and/or PROTOC_BINARY to use locally-installed versions instead.

If rust-toolchain.toml is removed, newer Rust versions can be used. Older versions
may or may not compile the code.

To build Anki fully offline, set the following environment variables:

- OFFLINE_BUILD: If set, the build does not run tools that may access
  the network.

- NODE_BINARY, YARN_BINARY and PROTOC_BINARY must also be set.

With OFFLINE_BUILD defined, manual intervention is required for the
offline build to succeed. The following conditions must be met:

1. All required dependencies (node, Python, rust, yarn, etc.) must be
   present in the build environment.

2. The offline repositories for the translation files must be
   copied/linked to ftl/qt-repo and ftl/core-repo.

3. The Python pseudo venv must be set up:

    ```
    mkdir out/pyenv/bin
    ln -s /path/to/python out/pyenv/bin/python
    ln -s /path/to/protoc-gen-mypy out/pyenv/bin/protoc-gen-mypy
    ```

    Optionally, set up your environment to generate Sphinx documentation:

    ```
    ln -s /path/to/sphinx-apidoc out/pyenv/bin/sphinx-apidoc
    ln -s /path/to/sphinx-build out/pyenv/bin/sphinx-build
    ```

    Note that the PYTHON_BINARY environment variable need not be set,
    since it is only used when OFFLINE_BUILD is unset to automatically
    create a network-dependent Python venv.

4. Create the offline cache for yarn and use its own environment
   variable YARN_CACHE_FOLDER to it:

    ```
    YARN_CACHE_FOLDER=/path/to/the/yarn/cache
    /path/to/yarn install --ignore-scripts
    ```

You are now ready to build wheels and Sphinx documentation fully
offline.

## More

For info on running tests, building wheels and so on, please see [Development](https://anki.mintlify.app/developers/development).

---

### Mac (`docs/mac.md`)

# Mac-specific notes



## Requirements

**Xcode**:

Install the latest XCode from the App Store. Open it at least once
so it installs the command line tools.

**Git/rsync**

Install via Homebrew or similar tool.

## More

For info on running tests, building wheels and so on, please see [Development](https://anki.mintlify.app/developers/development).

---

### Ninja (`docs/ninja.md`)

# Ninja



Brief notes for people used to the existing Bazel build system:

- Put the ninja binary on your path: https://github.com/ninja-build/ninja/releases/tag/v1.11.1
  (on Windows, if you have it installed in msys, make sure the native binary occurs earlier on the path)
- Ensure Rust is installed via rustup: https://rustup.rs/
- Remove the .bazel and node_modules folders from your existing checkout

- Run with ./run
- Run tests with './ninja check' (tools\ninja on Windows)
- Format files with './ninja format'
- Fix eslint/copyright issues with './ninja fix'
- Targets are hierarchical, so './ninja check:jest:deck-options' will run
  the Jest tests for ts/deck-options, and './ninja check:jest' will run all
  Jest tests.

---

### Protobuf (`docs/protobuf.md`)

# Protocol Buffers



ProtoBuf is a format used both to save data in storage and transmit
data between services. You can think of it as similar to JSON with
schemas, given that you can use basic types, list and records. Except
that it's usually transmitted and saved in an efficient byteform and
not in a human readable way.

# Protocol Buffers

Anki uses [different implementations of Protocol Buffers](https://anki.mintlify.app/developers/architecture#protobuf)
and each has its own peculiarities. This document highlights some aspects relevant
to Anki and hopefully helps to avoid some common pitfalls.

For information about Protobuf's types and syntax, please see the official [language guide](https://developers.google.com/protocol-buffers/docs/proto3).

## General Notes

### Names

Generated code follows the naming conventions of the targeted language. So to access
the message field `foo_bar` you need to use `fooBar` in Typescript and the
namespace created by the message `FooBar` is called `foo_bar` in Rust.

### Optional Values

In Python and Typescript, unset optional values will contain the type's default
value rather than `None`, `null` or `undefined`. Here's an example:

```protobuf
message Foo {
  optional string name = 1;
  optional int32 number = 2;
}
```

```python
message = Foo()
assert message.number == 0
assert message name == ""
```

In Python, we can use the message's `HasField()` method to check whether a field is
actually set:

```python
message = Foo(name="")
assert message.HasField("name")
assert not message.HasField("number")
```

In Typescript, this is even less ergonomic and it can be easier to avoid using
the default values in active fields. E.g. the `CsvMetadata` message uses 1-based
indices instead of optional 0-based ones to avoid ambiguity when an index is `0`.

### Oneofs

All fields in a oneof are implicitly optional, so the caveats [above](https://anki.mintlify.app/developers/protobuf#optional-values)
apply just as much to a message like this:

```protobuf
message Foo {
    oneof bar {
      string name = 1;
      int32 number = 2;
    }
}
```

In addition to `HasField()`, `WhichOneof()` can be used to get the name of the set
field:

```python
message = Foo(name="")
assert message.WhichOneof("bar") == "name"
```

### Backwards Compatibility

The official [language guide](https://developers.google.com/protocol-buffers/docs/proto3)
makes a lot of notes about backwards compatibility, but as Anki usually doesn't
use Protobuf to communicate between different clients, things like shuffling around
field numbers are usually not a concern.

However, there are some messages, like `Deck`, which get stored in the database.
If these are modified in an incompatible way, this can lead to serious issues if
clients with a different protocol try to read them. Such modifications are only
safe to make as part of a schema upgrade, because schema 11 (the targeted schema
when choosing _Downgrade_), does not make use of Protobuf messages.

### Field Numbers

Field numbers larger than 15 need an additional byte to encode, so `repeated` fields
should preferably be assigned a number between 1 and 15. If a message contains
`reserved` fields, this is usually to accommodate potential future `repeated` fields.

## Implementation-Specific Notes

### Python

Protobuf has an official Python implementation with an extensive [reference](https://developers.google.com/protocol-buffers/docs/reference/python-generated).

### Typescript

Anki uses [protobuf-es](https://github.com/bufbuild/protobuf-es), which offers
some documentation.

### Rust

Anki uses the [prost crate](https://docs.rs/prost/latest/prost/).
Its documentation has some useful hints, but for working with the generated code,
there is a better option: From within `anki/rslib` run `cargo doc --open --document-private-items`.
Inside the `pb` module you will find all generated Rust types and their implementations.

- Given an enum field `Foo foo = 1;`, `message.foo` is an `i32`. Use the accessor
  `message.foo()` instead to avoid having to manually convert to a `Foo`.
- Protobuf does not guarantee any oneof field to be set or an enum field to contain
  a valid variant, so the Rust code needs to deal with a lot of `Option`s. As we
  don't expect other parts of Anki to send invalid messages, using an `InvalidInput`
  error or `unwrap_or_default()` is usually fine.

---

### Releasing (`docs/releasing.md`)

# Releasing



Releases are prepared with a script that a maintainer runs locally, then built
and published by a GitHub Actions workflow:

1. **`tools/prepare_release.py`** — Run first, on your own machine.
   Validates the version, checks out the matching `release/YY.MM` branch, checks
   that CI passed, makes sure no duplicate tag or release exists, syncs
   translations, then commits the updated `.version` and pushes the branch. CI
   then runs automatically on the resulting commit (for `release/**` branches).
   The CI check can be skipped with `--skip-ci-check`.

2. **`.github/workflows/release.yml`** — Dispatch after CI passes on the
   prepared commit. Builds installers and wheels for all platforms (Linux
   x86/ARM, macOS Intel/ARM, Windows), and can optionally sign macOS/Windows
   artifacts, create a draft GitHub release, publish wheels to TestPyPI, and
   publish wheels to PyPI.

`release.yml` is `workflow_dispatch` and uses a `release`
concurrency group, so two release builds cannot run simultaneously.

## Prepare script requirements

The prepare script runs on your machine, so it needs:

- A clean working tree. The script stops if there are uncommitted changes.
- An authenticated `gh` CLI, with push access to `ankitects/anki` and to the two
  translation repos.
- `rsync` on the `PATH`. It copies the translation templates.
- A Python interpreter with the `packaging` module, which the project's `uv`
  environment provides.

The script pushes translation template commits to the translation repos before
it commits `.version`, so a failure after that point leaves those pushes in
place. Re-running the script is safe: the translation step is a no-op when there
is nothing new to copy.

## Version format

Versions follow calendar versioning with PEP 440: `YY.MM` for stable releases
(e.g. `26.04`), with optional `.patch` (e.g. `26.04.1`) and pre-release
suffixes (`b1`, `rc1`, `a1`). Months must be zero-padded.

Examples: `26.05b1` (beta), `26.05rc1` (release candidate), `26.05` (stable),
`26.05.1` (patch).

## Release branch workflow

All releases are cut from a `release/YY.MM` branch. The branch name uses only
the major version (`YY.MM`), not the full pre-release suffix — betas, release
candidates, and the stable release all come from the same branch.

### Standard release

1. Create a release branch from `main`:

    ```
    git checkout -b release/26.05 main
    git push origin release/26.05
    ```

2. CI runs automatically on push to `release/**` branches.

3. Prepare the release (updates `.version` on the branch):

    ```
    just release::prepare --version 26.05b1
    ```

4. Verify on TestPyPI:

    ```
    just release::testpypi --ref release/26.05
    ```

5. Publish the full release:

    ```
    just release::public --ref release/26.05
    ```

6. For subsequent pre-releases or the stable release from the same cycle,
   repeat steps 3-5 with the new version (e.g. `26.05b2`, `26.05rc1`, `26.05`).

7. Merge the release branch back to `main`. When a stable release is published,
   `.github/workflows/sync-release.yml` opens a PR from `release/26.05` to
   `main`, which brings in the `.version` bump, the translation updates, and any
   cherry-picked fixes. Review the PR and merge it.

8. Delete the release branch after the merge PR is merged.

9. Submit the Flathub update. When a stable release is published,
   `.github/workflows/sync-release.yml` makes a patch that moves
   [flathub/net.ankiweb.Anki](https://github.com/flathub/net.ankiweb.Anki)
   to the new version. The workflow uploads the patch as an artifact, and
   comments on the release PR with a report and the commands to open the
   Flathub PR. Fix the problems in the report's "Needs attention" section, if
   there are any. See
   [updating-anki.md](https://github.com/flathub/net.ankiweb.Anki/blob/master/updating-anki.md)
   for the manual steps.

10. Approve the website update. When a stable release is published, the
    `update-website` job in `.github/workflows/sync-release.yml` sets the
    download version in
    [ankitects/anki-landing-page](https://github.com/ankitects/anki-landing-page)
    to the new version, then pushes the change. This job uses the `release`
    environment, so a reviewer must approve it before it runs.

### Security and hotfix releases

For security fixes, an admin should first create a
[security advisory](https://github.com/ankitects/anki/security/advisories)
with a temporary private fork. Work on the fix in the private fork via the
normal PR workflow. Do not open a public PR or publish the advisory until
the fix is ready for release.

Once the fix is ready:

1. Create a release branch from the latest release tag:

    ```
    git checkout -b release/26.05 26.05
    ```

2. Cherry-pick the fix onto the release branch.

3. Push the branch and wait for CI:

    ```
    git push origin release/26.05
    ```

4. Prepare and publish:

    ```
    just release::prepare --version 26.05.1
    just release::public --ref release/26.05
    ```

5. Merge the release branch back to `main`.

6. For security patches, publish the advisory and credit the reporter if
   applicable.

## Release process overview

```{mermaid}
flowchart LR
    A["<b>prepare_release.py</b><br/><i>local</i><br/>validate version<br/>check out release branch<br/>check CI<br/>check duplicate tag<br/>sync translations<br/>update .version<br/>push to branch"] --> B["<b>CI (ci.yml)</b><br/>runs automatically<br/>on release/** branches"]
    B --> C["<b>release.yml</b><br/>build all platforms<br/>optionally sign macOS/Windows<br/>optionally create draft GitHub release<br/>optionally publish to TestPyPI/PyPI"]

    style A fill:#2d333b,stroke:#539bf5,color:#adbac7
    style B fill:#2d333b,stroke:#e5c07b,color:#adbac7
    style C fill:#2d333b,stroke:#7ee787,color:#adbac7
```

## Release workflow jobs

```
/* Detailed source-code truncated for AI context efficiency. */
```

## Inputs

**prepare_release.py:** takes a `version` argument and an optional
`--skip-ci-check` flag. The release branch is derived from the version, so there
is no branch argument.

**release:** takes a `version` (must match `.version` for public release
operations) and five boolean inputs:

- `sign` signs macOS and Windows artifacts.
- `draft-release` creates the draft GitHub release.
- `publish-testpypi` publishes wheels to TestPyPI.
- `publish-pypi` publishes wheels to PyPI.
- `skip-ci-check` skips the CI status check.

All booleans default to `false`. Non-release runs use the `.version`
already in the repo, so builds work without a prepare step.

For a normal public release, enable the first four booleans: `sign=true`,
`draft-release=true`, `publish-testpypi=true`, and `publish-pypi=true`.

## Environment gates

The release workflow uses GitHub
[environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)
as manual approval gates. Jobs that access signing credentials or publish
artifacts require a reviewer to approve the deployment before they run:

- **`release`** — Required when `sign`, `draft-release`, `publish-testpypi`, or
  `publish-pypi` is enabled. Protects code-signing secrets, the release token,
  and PyPI/TestPyPI trusted publishing/OIDC. The `update-website` job in
  `sync-release.yml` also uses it, to protect the landing page deploy key.

When `sign` is disabled, the macOS and Windows build jobs run without the
`release` environment so they do not require approval and cannot access signing
secrets.

## Workflow input behavior

The `release.yml` workflow uses independent boolean inputs to control what gets
signed and published:

| Input              | Effect                                                                                                                                                                                                                                            |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sign`             | Signs macOS and Windows artifacts. Requires the `release` environment. When false, those jobs upload unsigned artifacts and do not access signing secrets.                                                                                        |
| `draft-release`    | Creates a draft GitHub release with generated release notes and installer artifacts. Requires `sign=true`, the `release` environment, passing CI (unless skipped), no duplicate tag/release, and `version` matching `.version`.                   |
| `publish-testpypi` | Publishes wheels to TestPyPI. Requires the `release` environment.                                                                                                                                                                                 |
| `publish-pypi`     | Publishes wheels to PyPI. Requires the `release` environment, passing CI (unless skipped), and `version` matching `.version`. It also runs and waits for the TestPyPI publish job first. It does not require signing unless `draft-release=true`. |
| `skip-ci-check`    | Skips the CI status check. Useful for hotfix releases.                                                                                                                                                                                            |
| `version`          | For `draft-release` or `publish-pypi`: must match `.version`. For build-only, signed-only, or TestPyPI-only runs: ignored (`.version` from the branch is used automatically).                                                                     |

## Running releases with just

The `release` module defined in `release.just` wraps both the prepare script and
the `release.yml` dispatches. `release::prepare` runs locally and takes no
`--ref`. Every other recipe dispatches `release.yml` and requires an explicit
`--ref` argument pointing to the release branch.

Run `just --list --list-submodules` to see all available recipes and their
arguments.

## Testing the release workflow from a feature branch

`release.yml` can be dispatched from any branch for testing. The release guards
only apply when `draft-release` or `publish-pypi` is enabled. To run a test
build:

1. Dispatch `release.yml` from your branch with all boolean inputs left false:
    ```
    just release::build --ref <your-branch>
    ```
2. The workflow reads `.version` from the branch as-is (the version input is
   ignored for non-release runs), so no prepare step is needed.
3. All release guards (CI check, duplicate tag check) are skipped.
4. Artifacts are uploaded to the workflow run but nothing is published or tagged.

### Testing with code signing

To test the signing flow from a feature branch:

1. In the repo's Settings → Environments → `release`, temporarily add your
   branch to the allowed deployment branches.
2. Dispatch the workflow:
    ```
    just release::sign --ref <your-branch>
    ```
3. Approve the environment deployment when prompted.
4. After testing, remove your branch from the environment's allowed branches.

> **Note:** `workflow_dispatch` workflows only appear in the GitHub Actions UI
> if the workflow file exists on the default branch. If `release.yml` is new or
> modified on your branch, use `gh workflow run` to trigger it — the UI
> dropdown won't show it until it's merged to main.

## Important notes

- The release workflow builds the exact commit at `github.sha`. It does not
  write `.version` — that is done by the prepare script. If you dispatch release
  before the prepare commit has been pushed, the build will use whatever
  `.version` was HEAD at dispatch time.
- `draft-release=true` with `sign=false` is rejected — draft releases must use
  signed installer artifacts.
- When `publish-pypi=true`, wheels are published to TestPyPI first, then to
  PyPI after the TestPyPI job succeeds. If `draft-release=true` is also set,
  PyPI publishing waits for the draft GitHub release to succeed too.
- Pre-release versions (e.g. `26.05b1`) are automatically marked as
  pre-releases on the GitHub draft release.

## Announcements

- Once a GitHub release draft is created, modify the generated changelog if necessary then click **Publish release**.
- Create a forum topic on the [Beta Testing](https://forums.ankiweb.net/c/anki/beta-testing/13) category. For stable releases, lock the topic and ask users to report issues on a new topic.

## Audio package

Audio playback and recording is handled by the `anki-audio` package under **`qt/audio`**, which bundles mpv and lame for Windows and macOS. To update:

1. Update the archive links in **`build/configure/src/audio.rs`** if you're updating the Windows pre-built binaries to new versions, then run `./tools/ninja audio_wheel` to test.
2. On macOS, mpv, lame and all their libraries are built from source. Update the pinned versions and checksums in **`qt/audio/macos/versions.env`**, then test locally by running **`qt/audio/build.sh`** and **`qt/audio/macos/test-in-vm.sh`**.
3. Bump the version at **`qt/audio/.version`**.
4. Submit a PR with the changes.
5. Once the PR is merged to main, run the **`.github/workflows/publish-audio-package.yml`** workflow to publish the package to PyPI:
    ```
    gh workflow run publish-audio-package.yml -f sign-macos=true -f publish-testpypi=true -f publish-pypi=true
    ```
6. Once the package is published, update the pinned `anki-audio` version in **`qt/pyproject.toml`** to the new version.

---

### Sonarcloud (`docs/sonarcloud.md`)

# SonarCloud analysis



SonarCloud provides informative static-analysis and coverage results. Its
Quality Gate is not a required merge check, so findings guide review without
blocking maintainers. Automatic Analysis must remain disabled because this
project uses CI-based analysis.

For the test commands and coverage thresholds enforced by CI, see
[Testing and Coverage](https://anki.mintlify.app/developers/testing-coverage).

## Analysis matrix

| Event                          | Static analysis | Coverage in CI | Coverage in SonarCloud |
| ------------------------------ | --------------- | -------------- | ---------------------- |
| Internal pull request          | Yes             | Yes            | Yes                    |
| Push to `main` or `release/**` | Yes             | Yes            | Yes                    |
| Fork pull request              | Yes             | Yes            | No                     |

Fork pull requests still run the complete test coverage and coverage-regression
checks in the unprivileged CI workflow. Their reports are not imported by the
privileged SonarCloud workflow, so SonarCloud does not calculate a coverage
metric for those pull requests.

The fork scan still reports static bugs, vulnerabilities, security hotspots,
and code smells. Scanner-side Clippy and software composition analysis are
disabled; Clippy remains part of the regular CI checks.

## How it runs

`.github/workflows/sonar.yml` runs after the complete CI workflow succeeds.
Keeping it separate prevents `SONAR_TOKEN` from being available to jobs that
build or test contributor code.

1. The workflow validates the pull request number, state, head SHA,
   repositories, and branches against the GitHub API. Invalid or stale context
   fails closed instead of becoming a branch analysis.
2. It checks out the exact commit tested by CI. Fork source is read only by the
   scanner and is never built, installed, or used as tooling.
3. It replaces `sonar-project.properties` with the default branch's trusted
   copy, preventing a fork from changing the server or enabling build tools.
   The analysed version comes from the same trusted checkout of `.version`, so
   that new code is measured since the previous release rather than since a
   constant version.
4. For a pull request, it fetches the target branch from the trusted upstream
   repository and pins it locally under the name passed as
   `sonar.pullrequest.base`. SonarCloud derives the changed-file set from git,
   so without the correct upstream base reference it may attribute unrelated
   changes to the pull request. A fork's local base reference may be outdated.
   The step logs the analysed commit, base repository and branch, base commit,
   and merge-base, and fails closed if the reference cannot be resolved.
5. For internal changes, it downloads the coverage artifact from the triggering
   CI run. For forks, report paths are cleared and all sources are excluded only
   from the coverage calculation.
6. Only the scanner step receives `SONAR_TOKEN`.

## Trust boundary

<Warning>
    This workflow has access to the base repository's `SONAR_TOKEN`. Never add a build,
    test, package install, PR-provided configuration, local Action from the PR, or
    restored CI cache to it. In particular, do not invoke `just`, `cargo`, `npm`, `pip`,
    or repository scripts in this workflow.
</Warning>

- Keep Automatic Analysis disabled and SonarCloud checks non-required.
- Restrict `SONAR_TOKEN` to analysis of this project.
- Keep scanner-side Clippy and software composition analysis disabled.
- Do not import artifacts produced by a fork into the privileged scan.

The scanner necessarily parses untrusted source when analyzing a fork. These
constraints prevent direct execution of contributor code and reduce the token's
exposure, but they do not eliminate vulnerabilities in third-party Actions or
analyzers.

---

### Testing Coverage (`docs/testing-coverage.md`)

# Testing and Coverage



CI runs Rust, Python, and TypeScript tests plus lint/type checks. Coverage is
orchestrated in `justfile` with direct CLI calls, using ninja only to prepare
generated build artifacts that the test commands need.

## Quick reference

```sh
just test                        # run all tests (no coverage)
just test --coverage             # run all tests + enforce coverage thresholds
just test --coverage --html      # same + generate HTML reports under out/coverage/

just test-rust                   # Rust only
just test-rust --coverage
just test-rust --coverage --html

just test-py                     # Python (pylib + qt) only
just test-py --coverage
just test-py --coverage --html

just test-ts                     # TypeScript/Svelte Vitest only
just test-ts --coverage
just test-ts --coverage --html
```

HTML reports are written under `out/coverage/` (gitignored).

## Coverage tools and thresholds

| Stack               | Test runner                          | Coverage tool    | Minimum |
| ------------------- | ------------------------------------ | ---------------- | ------: |
| Rust workspace      | `cargo nextest` via `cargo-llvm-cov` | `cargo-llvm-cov` |     60% |
| Python `pylib/anki` | `pytest pylib/tests`                 | `coverage.py`    |     63% |
| Python `qt/aqt`     | `pytest qt/tests`                    | `coverage.py`    |     20% |
| TypeScript/Svelte   | `vitest run`                         | Vitest V8        |      5% |

Linux pull requests run `just test --coverage` in CI. macOS and Windows
jobs run `just test` (no coverage enforcement) for now.

## Notes

- **Rust** — `cargo-llvm-cov` is installed on demand into `out/bin/` to avoid
  polluting the global cargo install. Coverage runs rebuild the workspace with
  instrumentation, so they are slower than plain `just test-rust`.
  **Windows ARM64 (`aarch64-pc-windows-msvc`) is not supported**: the Rust
  compiler produces malformed `.profraw` files on that target
  ([rust-lang/rust#150123](https://github.com/rust-lang/rust/issues/150123),
  [cargo-llvm-cov#436](https://github.com/taiki-e/cargo-llvm-cov/issues/436)).
  `just test-rust --coverage` exits with a clear message on ARM64 Windows;
  use `just test-rust` (no coverage) or rely on CI (Linux) for enforcement.
- **Python** — coverage is split across two suites (`pylib` and `qt`) because
  they have different `PYTHONPATH` setups and test folders.
- **TypeScript** — coverage is measured only over code reachable through
  Vitest's module graph. Svelte component rendering behavior is not covered.

## SonarCloud

SonarCloud runs automatically after successful CI pushes and pull requests. It
runs static analysis across Python, TypeScript, and Rust. For internal pull
requests and trusted branch pushes, it also aggregates the coverage reports
generated by the three stacks.

- **Dashboard**: https://sonarcloud.io/project/overview?id=ankitects_anki
- **Internal coverage**: uses the same LCOV/XML reports produced by `just test --coverage`
- **Fork coverage**: remains enforced in CI but is not imported into SonarCloud
- **Analysis results**: SonarCloud reports quality gate status on each analyzed PR

The quality gate is configured as non-blocking — a failing gate does not prevent
merging, but the results are visible on the PR.

See [SonarCloud analysis](https://anki.mintlify.app/developers/sonarcloud) for the analysis matrix and the fork trust
boundary.

## Gaps and future improvements

- Raise thresholds gradually as the test suite grows and CI timings stabilise.
- Exclude generated files from coverage denominators where appropriate.
- Publish `out/coverage/` as a CI artifact so reviewers can browse HTML
  reports directly from a PR.
- Consider diff/changed-file coverage once baselines are stable — it is a
  better enforcement mechanism for incremental improvement than whole-repo
  thresholds.
- Add component or browser tests for Svelte UI surfaces if Svelte coverage
  is intended to cover rendered component behaviour.

## See also

- [Writing Unit Tests for Anki](https://anki.mintlify.app/developers/unit-testing)

---

### Unit Testing (`docs/unit-testing.md`)

# Writing Unit Tests for Anki



This guide helps maintainers, contributors, and coding assistants write unit and
component tests for Anki. It is independent of any particular feature, module, or
bug. Use it together with the behavior being changed, the surrounding production
code, and nearby tests.

This guide assumes basic familiarity with automated tests. If testing is new to you,
start with one of the introductory resources in [References](https://anki.mintlify.app/developers/unit-testing#references-and-rationale),
then return here for Anki-specific guidance.

## Quick summary

When adding a test:

1. Describe what should happen from the user's point of view.
2. Add the test close to the code responsible for that behavior.
3. Consider normal input, errors, edge cases, and known regressions.
4. Prefer the smallest and fastest test that gives enough confidence.
5. Give the test a name that explains the behavior being checked.
6. Avoid dependencies on real time, network access, user data, or test order.
7. For a bug fix, confirm that the test fails before the fix when practical.
8. Run the relevant `just test-*` recipe and then `just check`.

## Purpose

A useful test gives fast, trustworthy evidence about behavior that matters. It also
acts as executable documentation: a reader should be able to understand the
scenario, action, and expected result without reverse-engineering the implementation.

A good unit test is:

- **Behavioral:** it verifies an observable result, state transition, error, or
  required interaction.
- **Focused:** it has one reason to fail. Multiple assertions are fine when they
  describe one behavior.
- **Deterministic:** the same code and inputs produce the same result regardless of
  test order, wall-clock time, locale, network access, or machine speed.
- **Isolated:** it does not depend on a user's profile, another test's state, or an
  unavailable external service.
- **Fast:** it is cheap enough to run repeatedly while editing.
- **Readable:** its name and test data explain the expected behavior and the failure.
- **Sensitive:** a real regression in the behavior makes it fail; it does not keep
  passing when the expected behavior is broken.
- **Stable:** a behavior-preserving refactor should usually leave it unchanged.

Code coverage is useful for finding untested code, but a covered line is not
necessarily a tested behavior. Do not add weak assertions merely to increase a
coverage number.

## Choose the smallest useful test boundary

Use the lowest-cost test that can prove the behavior:

| Boundary                | Use it for                                                                                                                  | Do not use it for                                       |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Pure unit               | Calculations, parsing, validation, conversion, and deterministic state transitions                                          | Framework wiring or cross-process behavior              |
| Unit with collaborators | A small group of fast, in-process collaborators                                                                             | Network, real subprocesses, or a user's persistent data |
| UI component            | A Qt component's visible state, signals/events, and interaction; for Svelte, extracted logic and stores                     | Whole application workflows                             |
| Integration             | Database mappings, protobuf/language bridges, filesystem contracts, and framework integration that a unit test cannot prove | Repeating every unit-level branch                       |
| End-to-end              | A small number of critical workflows through a real Anki instance and browser                                               | Exhaustive validation and edge cases                    |

Most scenarios should be below the end-to-end layer. If a higher-level test discovers
a defect, add a lower-level regression test when that level can reproduce the defect.
Keep the higher-level test only when it proves additional behavior.

For Svelte specifically, "UI component" means extracted logic and stores: the
repository does not wire rendered-component tests into the unit suite (see
[Svelte, TypeScript, and JavaScript](https://anki.mintlify.app/developers/unit-testing#svelte-typescript-and-javascript)).

Anki spans several implementation layers. Test a rule at the layer that owns it:

- Business rules implemented in Rust **should** normally be tested in Rust.
- Python library tests should cover Python-owned behavior, compatibility surfaces,
  orchestration, and adaptations instead of duplicating Rust assertions.
- Python/Qt tests should cover GUI-owned state, signals, callbacks, and backend
  orchestration. Move framework-independent logic into a form that can be tested
  without constructing the full GUI when practical.
- TypeScript/JavaScript tests should cover web-owned logic, stores, DOM helpers, and
  component behavior. Use end-to-end tests only when a real browser/Anki boundary is
  essential.
- Generated protobuf code does not need tests. Test custom conversion, validation,
  or bridge behavior at its owning layer; use an integration test if correctness
  depends on both sides of the bridge.

## Choose scenarios from expected behavior

Before writing test code, describe what should happen in plain English. Identify the
inputs, results a caller or user can observe, side effects, errors, and facts that
must remain true. Then select the smallest set of scenarios that distinguishes a
correct implementation from a plausible bug.

Every meaningful behavior change should consider these categories:

| Category           | Question                                                                | Generic example                                                                    |
| ------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Happy path         | Does valid, representative input produce the intended result?           | An accepted value is normalized and stored.                                        |
| Unhappy/error path | Is invalid input rejected or recovered from as specified?               | A malformed value returns the expected error and does not mutate state.            |
| Boundary           | What happens at empty, minimum, maximum, transition, and Unicode cases? | An empty label uses the defined fallback; the maximum allowed value remains valid. |
| State/invariant    | What must remain true before and after the action?                      | A failed update leaves the previous value and unrelated records unchanged.         |
| Regression         | What is the smallest input that reproduces a reported defect?           | The exact shape that once crashed now produces the specified result.               |

Do not assume that every function needs one test from every category. Include a
category when the expected behavior or implementation has that failure mode.

Use equivalence classes rather than enumerating inputs that exercise the same rule.
Use parameterization when several compact input/output pairs express one behavior;
use separate tests when cases require different setup or should produce distinct
failure explanations.

## Structure and naming

Prefer a visible Arrange–Act–Assert flow:

1. **Arrange** only the state relevant to the scenario.
2. **Act** once on the behavior under test.
3. **Assert** the expected outcome, including important non-effects.

The phases do not require comments when the structure is already obvious. Avoid
hiding the act or the important assertion inside a helper.

Name a test after the unit under test and the expected behavior, not an
implementation method. Follow the convention of the tests already in that file;
newer tests in the codebase put the condition last:

```text
<unit>_<expected_outcome>[_when_<condition>]
```

Real examples from the codebase:

- Rust: `answer_easy_graduates_new_card_to_review_queue`,
  `bury_leaves_suspended_card_untouched`,
  `add_or_update_notetype_preserves_usn_when_flag_is_set`
- Python/Qt: `test_update_collection_skips_backend_when_unchanged`,
  `test_is_audio_file_rejects_no_extension`

Vitest names the behavior in the `test(...)` description string rather than in an
identifier, e.g. `test("event handler attributes are stripped")`.

Names such as `test_helper`, `works`, `basic`, or `case_1` do not explain the
expected behavior.

Assertions should be precise enough to diagnose the defect. Prefer comparing the
meaningful value or structured result over a generic truthiness check. Assert an
error's type or variant and, when stable and user-relevant, its message. Avoid
asserting incidental formatting or a complete object when only one field defines the
behavior.

## Test data, setup, and cleanup

- Use the smallest realistic data and give it semantic names such as
  `existing_record` and `invalid_name`.
- Prefer builders, factories, and fixtures already used by nearby tests.
- Keep test-specific setup in the test. Extract a helper only when it removes noise
  without concealing the scenario.
- Use temporary directories, temporary collections/databases, and in-memory values.
  Never read or modify a user's Anki profile.
- Restore patched globals, environment variables, timers, DOM state, and callbacks.
  Prefer framework cleanup facilities that run even after an assertion fails.
- Tests **must** pass independently, in any order, and when repeated.

## Test doubles and boundaries

Prefer real, lightweight, deterministic collaborators. Use a stub, fake, spy, or mock
to control an awkward boundary such as time, randomness, network, subprocess,
clipboard, native dialog, or backend call, and patch it where the code looks it up.
Return realistic values and errors.

Assert interactions only when they affect expected behavior; avoid incidental calls
and ordering. If every collaborator is mocked and only calls are asserted, the test
probably describes the implementation rather than useful behavior.

## Time, randomness, concurrency, and asynchronous behavior

- Inject or freeze time when the exact time affects behavior. Do not depend on the
  current date, local timezone, or rollover hour.
- Seed or replace randomness and assert invariants rather than one accidental random
  result.
- Never use an unconditional sleep to wait for correctness. Await the promise,
  callback, signal, task, or condition with a bounded safety timeout.
- Avoid exact performance timings in unit tests. Put performance claims in a
  benchmark with appropriate tolerance and environment control.
- Assume files and test cases may run in parallel. Do not share mutable globals,
  fixed ports, or fixed temporary paths.

## Regression tests

A bug fix **should** normally include a test that would have caught the bug.

1. Reduce the report to the smallest representative input and state.
2. Place the test at the lowest layer that reproduces the defect and proves the
   intended behavior.
3. When practical, run it before the fix or against the buggy revision and confirm
   that it fails for the expected reason.
4. Apply the fix and confirm that the new test passes.
5. Run the relevant suite and check adjacent scenarios for the same class of defect.
6. Keep the regression test permanently. Name it after the behavior, not an issue
   number; add the issue link in a short comment only when it provides essential
   context that the test cannot express.

Do not encode the buggy implementation in the expected result. A regression test
should describe lasting expected behavior so that future refactoring can preserve it.

## Test-first workflow

Writing the test before the implementation is encouraged when the expected behavior
is clear. A test-first cycle can help verify that the test is sensitive to the defect:

1. Write a focused test for the intended behavior.
2. Confirm that it fails for the expected reason.
3. Make the smallest production change that satisfies the expected behavior.
4. Refactor while keeping the test suite green.

Test-first development is not mandatory. For exploratory work or behavior that is
not yet understood, investigate and describe the expected behavior before adding the
test. For bug fixes, a failing regression test before the fix is strongly recommended
whenever practical.

## Additional risk areas

The scenario categories above apply to all behavior. Also consider risks specific to
Anki:

- serialization or conversion code written by the project;
- permission, path, escaping, and untrusted-input boundaries;
- callbacks, signals, events, and async completion when they are part of the API;
- compatibility behavior intentionally supported for add-ons or older data.

## What not to test

Do not spend test maintenance on:

- private implementation details with no observable contract;
- trivial code, compiler/type-checker guarantees, generated accessors, or third-party
  behavior;
- the same rule exhaustively at every language layer;
- snapshots so broad that reviewers cannot tell whether a change is correct;
- real network services, user profiles, home-directory files, or machine-specific
  executables;
- random input without a reproducible seed and reported failing case;
- internal call counts used only because mocking makes them easy to observe; or
- assertions that only prove the code ran, returned something truthy, or did not
  throw when a more specific result exists.

## UI testing scope and cost

Qt and Svelte tests have a higher setup and maintenance cost than tests for pure
logic. Do not attempt to test every widget, component, property, or markup detail. A
UI test **should** protect behavior whose value and risk justify that cost.

Prefer testing:

- logic, view models, stores, state transitions, and error handling;
- interactive states, signals, events, callbacks, and backend requests;
- accessibility roles, labels, focus, and keyboard behavior when they are part of
  the user contract; and
- regressions in stable, user-visible behavior.

Avoid unit tests for styling, exact geometry, incidental DOM structure or CSS/Sass
class names, animation timing, platform-native rendering, generated UI code,
framework internals, and large UI snapshots.

When possible, extract logic from the Qt widget or Svelte component and test it as
ordinary Python or TypeScript/JavaScript. Keep only a small wiring test at the UI
layer when it provides additional confidence.

Check presentation during UI review, or with visual regression if appearance is a
stable product contract and the project intentionally adopts such tooling. Volatile
code is not automatically exempt: test its stable contract when one exists. If both
implementation and expected presentation are temporary, omitting a brittle UI test
may be the better trade-off.

## Stack-specific guidance

The examples below are illustrative pseudocode. Names such as `State`, `Model`, and
`normalize_value` are not Anki APIs and **must not** be copied without inspecting the
actual code.

### Python library

Python library tests live under `pylib/tests/` and use `pytest`.

- Name files/functions `test_*` and use plain `assert` for diagnostic diffs.
- Use `pytest.raises()` for expected errors; match a stable message only when the
  message is part of the contract or improves precision.
- Use `pytest.mark.parametrize()` for compact equivalent cases.
- Prefer typed fixtures, `tmp_path`, `monkeypatch`, and existing collection helpers;
  close resources through fixtures or context managers.
- Mock the Rust backend only when testing Python-owned orchestration or an otherwise
  impractical failure. Do not mock it merely to make a domain-rule test appear unit
  sized.

Generic example:

```python
import pytest


@pytest.mark.parametrize(
    ("raw_value", "expected"),
    [(" valid ", "valid"), ("", "default")],
)
def test_normalization_returns_the_documented_value(
    raw_value: str, expected: str
) -> None:
    assert normalize_value(raw_value) == expected


def test_invalid_value_is_rejected_without_mutating_state() -> None:
    state = State(value="existing")

    with pytest.raises(ValueError, match="invalid value"):
        state.update("invalid")

    assert state.value == "existing"
```

### Python/Qt

Python/Qt tests live under `qt/tests/` and also use `pytest`.

- Test framework-independent decisions as ordinary Python first.
- Construct the smallest useful Qt object; do not launch a complete Anki window for
  a unit test.
- Trigger public actions or realistic input, then assert meaningful state, signals,
  callbacks, or backend requests. Use `QSignalSpy` or a callback recorder when signal
  arguments/count are the contract.
- Await the event loop through a condition or signal with a bounded timeout; avoid
  fixed sleeps.
- Replace native dialogs, clipboard access, web requests, audio, and backend
  operations at their boundary.
- Keep `QApplication`, main-window state, and global hooks isolated and cleaned up.

Generic example:

```python
from unittest.mock import MagicMock


def test_submit_with_invalid_input_shows_error_without_saving() -> None:
    save = MagicMock()
    dialog = make_minimal_dialog(save=save)
    dialog.form.value.setText("invalid")

    dialog.accept()

    assert dialog.form.error.isVisible()
    save.assert_not_called()
```

### Rust

Rust unit tests normally live beside the code in a `#[cfg(test)]` module. Follow the
nearby crate's existing helpers and conventions.

- Prefer inputs and assertions over extensive mocking.
- Use `assert_eq!`/`assert_ne!` for diagnostic diffs and include a message when the
  invariant is otherwise unclear.
- Match the expected error variant or structured value instead of only calling
  `is_err()` when the distinction matters.
- Cover variants and transitions with distinct behavior, not properties guaranteed
  by the type system.
- Use a collection only when persistence is part of the unit; use pure domain values
  otherwise.
- Use small table-driven loops for one contract and include the input in assertion
  messages.
- `unwrap()` and `expect()` **may** be used in setup and when an unexpected error
  should fail the test; do not use them to inspect the error path being tested.

Generic example:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_input_uses_the_default() {
        assert_eq!(normalize_value(""), "default");
    }

    #[test]
    fn invalid_input_preserves_existing_state() {
        let mut state = State::new("existing");

        let error = state.update("invalid").unwrap_err();

        assert!(matches!(error, UpdateError::InvalidValue));
        assert_eq!(state.value(), "existing");
    }
}
```

### Svelte, TypeScript, and JavaScript

Web unit tests use Vitest. The current configuration discovers colocated
`*.test.ts`, `*.spec.ts`, `*.test.js`, and `*.spec.js` files, excluding
`ts/tests/e2e/`.

- Test TypeScript/JavaScript logic and Svelte stores without a DOM unless rendering
  and interaction prove an additional contract.
- Add `// @vitest-environment jsdom` only when the test needs DOM APIs, and restore
  any document/global state after the test.
- Use `vi.fn()`/`vi.spyOn()` at external boundaries, fake timers for timer-driven
  behavior, and restore both after the test.
- Await promises/reactive updates instead of real time. For rendered behavior,
  interact through the public UI and query by accessible role, label, or visible text
  rather than internal state.
- Prefer focused value/DOM assertions. Use snapshots only for small, stable output
  whose entire structure is intentionally reviewed.
- The repository does not currently configure a dedicated Svelte component-rendering
  harness in the unit suite. If a behavior cannot be tested through extracted logic
  or the existing DOM setup, agree on the appropriate harness or place the essential
  browser contract in the existing Playwright suite rather than inventing a private
  test setup.

Generic TypeScript example:

```typescript
import { expect, test, vi } from "vitest";

test("invalid input reports the error without saving", async () => {
    const save = vi.fn();
    const model = new Model({ save });

    const result = await model.submit("invalid");

    expect(result).toEqual({ ok: false, reason: "invalid-value" });
    expect(save).not.toHaveBeenCalled();
});
```

For a Svelte component, the equivalent contract is expressed as a user action and a
visible/accessibility result: enter invalid input, activate Submit, observe the error,
and verify that saving did not occur. Do not call a component's private handler
directly.

## Running and validating tests

Use the repository's `just` recipes:

```sh
just test-rust  # Rust
just test-py    # Python library and Python/Qt
just test-ts    # TypeScript/JavaScript/Svelte unit tests
just test       # every stack (Rust, Python, TS)
just check      # required final formatting, build, lint, and test validation
```

During development, run the relevant stack frequently. Before considering a code
change complete, run `just check`. See [Testing and Coverage](https://anki.mintlify.app/developers/testing-coverage)
for coverage commands and [End-to-End Testing](https://anki.mintlify.app/developers/e2e-testing) when the
selected boundary requires Playwright.

## Review checklist

A test is ready when the answer to each relevant question is yes:

- Does the test name explain the behavior being checked?
- Does the test clearly show when that behavior works and when it does not?
- Were the relevant scenario categories considered at the lowest useful layer,
  without duplication?
- Is Arrange–Act–Assert clear, with minimal data and doubles only at meaningful
  boundaries?
- Is it deterministic, independent, cleaned up, and stable under behavior-preserving
  refactoring?
- For a bug fix, did the regression test fail before and pass after the fix when
  practical?
- Do the relevant `just` test recipe and `just check` pass?

## Instructions for coding assistants

When this document is supplied to a coding assistant, the assistant must also inspect
the production behavior, issue/diff, neighboring tests, available helpers, and test
runner configuration. This guide does not replace repository context.

Before editing, it should follow the quick summary, describe the expected behavior,
identify the responsible layer, list the scenarios and selected test level, then
follow local naming, fixtures, builders, and file placement. It **must not** invent
APIs, dependencies, fixtures, or runner capabilities; change production visibility
solely to reach private implementation; or weaken an assertion to make it pass. It
should finish by reporting what was tested, intentionally omitted, and not validated.

## References and rationale

This guide adapts these references to Anki's architecture:

- Beginner introductions: [Getting Started With Testing in Python](https://realpython.com/python-testing/)
  and [How to Use pytest](https://www.freecodecamp.org/news/how-to-use-pytest-a-guide-to-testing-in-python/).
- General principles: [behavior over implementation](https://testing.googleblog.com/2013/08/testing-on-toilet-test-behavior-not.html),
  [test sizes](https://testing.googleblog.com/2010/12/test-sizes.html),
  [unit-test characteristics and Arrange–Act–Assert](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices),
  and the [practical test pyramid](https://martinfowler.com/articles/practical-test-pyramid.html).
- Python: pytest [fixtures](https://docs.pytest.org/en/stable/explanation/fixtures.html),
  [parameterization](https://docs.pytest.org/en/stable/how-to/parametrize.html), and
  [assertions](https://docs.pytest.org/en/stable/how-to/assert.html).
- Rust: [test organization in The Rust Book](https://doc.rust-lang.org/book/ch11-03-test-organization.html).
- Qt: [QtTest](https://doc.qt.io/qtforpython-6/PySide6/QtTest/index.html) and
  [QSignalSpy](https://doc.qt.io/qt-6/qsignalspy.html).
- Web: Vitest [mock functions](https://vitest.dev/guide/learn/mock-functions),
  [timers](https://vitest.dev/guide/mocking/timers), and
  [component testing](https://vitest.dev/guide/browser/component-testing), plus
  Testing Library's [accessible query priority](https://testing-library.com/docs/queries/about/).

---

### Windows (`docs/windows.md`)

# Windows



## Minimum Requirements

- **64-bit Windows 10** (version 1703 or newer)
- Administrator access to install software

---

## Install Rust

1. Visit the official Rust website (https://rustup.rs/)
2. Download and install **rustup**
3. During installation, accept the default options

> [!NOTE]
> The Rust installer will offer to automatically download and install the required
> MSVC build tools. If you prefer a minimal install without the full Visual
> Studio IDE, do the following before installing Rust:
>
> 1. Install **Visual Studio Build Tools** via winget:
>     ```
>     winget install Microsoft.VisualStudio.BuildTools
>     ```
> 2. Open the **Visual Studio Installer**, select **Build Tools**, click
>    **Modify**, then under **Individual components**, install:
>     - **MSVC Build Tools for x64/x86 (Latest)**
>     - **Windows 11 SDK** (or Windows 10 SDK if you're on Windows 10)
>
> Then proceed with the Rust installation above.

> [!IMPORTANT]
> **Windows ARM users**: After installing rustup, run the following command in a terminal, inside the project folder:
>
> ```bash
> rustup target add x86_64-pc-windows-msvc
> ```

## Install MSYS2

1. Visit the [MSYS2](https://www.msys2.org/) website
2. Install it in the default location
3. After installation, open the MSYS2 UCRT64 terminal
4. Run the following command:

```bash
pacman -S git rsync
```

---

### Configure the PATH environment variable

1. Search for and open **Edit the system environment variables** in the Start menu
2. Click **Environment Variables**
3. Edit the **Path** variable under **System Variables**
4. Add the following path then reboot your computer: `C:\msys64\usr\bin`

> [!NOTE]
> If you have native Windows apps relying on Git, e.g. the PowerShell extension
> [posh-git](https://github.com/dahlbyk/posh-git), you may want to install
> [Git for Windows](https://gitforwindows.org/) and put it on the path instead,
> as msys Git may cause issues with them. You'll need to make sure rsync is
> available some other way.

## Choose a good source code location

Anki's source files do not need to be in a specific location, but it's best to
avoid long paths, as they can cause problems. Spaces in the path may cause
problems.

## More

For info on running tests, building wheels and so on, please see
[Development](https://anki.mintlify.app/developers/development).

---

--- METRICS ---
- Files Extracted: 37
- Estimated Token Budget: ~28526 tokens
- Canonical Reference: https://codewiki.google/github.com/ankitects/anki
