## Summary
Closes#4954.
`_functions.scss` already had `@use "sass:map"` at the top but was still
calling the deprecated global functions `map-get` and `map-merge`.
Replaced all occurrences with their namespaced equivalents `map.get` and
`map.merge`.
This eliminates the `[global-builtin]` deprecation warnings visible in
every Check (Linux) CI run, which will become hard errors in Dart Sass
3.0.
## Test plan
- [x] `just lint` passes
- [x] `just test-ts` passes with no deprecation warnings
## Before / after behavior (optional)
### at main branch
<img width="1325" height="906" alt="Image"
src="https://github.com/user-attachments/assets/6a35ac2d-418a-4ded-baf6-f959b06b8d48"
/>
### at this branch
<img width="1156" height="921" alt="image"
src="https://github.com/user-attachments/assets/276a15ce-b319-4ee5-9ae2-122cd4d4d45e"
/>
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue
Closes#4951
## Summary / motivation
Removes two dead `@import` statements from `_button-mixins.scss`.
Bootstrap's functions and variables were imported but never used, since
all mixins rely exclusively on CSS custom properties (`var(--)`) and
local mixin parameters.
This eliminates ~35 Dart Sass deprecation warnings (`[import]`,
`[color-functions]`) that appeared during `just test-ts --coverage`.
## Test plan
- [x] `just lint` passes
- [x] `just test-ts` passes with no deprecation warnings
## Before / after behavior (optional)
### current behavior (at main branch)
<img width="1424" height="979" alt="image"
src="https://github.com/user-attachments/assets/89fafb5e-e811-48ca-b7e0-dbae36f63d7a"
/>
### Expected behavior (at this branch)
<img width="1489" height="870" alt="image"
src="https://github.com/user-attachments/assets/3eca3296-d31b-4e02-ad5f-e9287e59ee3d"
/>
## Linked issue
Closes#4815
## Summary
This adds [Complexipy](https://github.com/rohaquinlop/complexipy) for
detecting complex Python code:
- The `check:complexity` Ninja actions use a high threshold (50) for now
to avoid failing on existing complex code.
- `just complexipy-diff` is intended for linting new code in PR CI and
uses 15 as the threshold. See
https://rohaquinlop.github.io/complexipy/usage-guide/#ratchet-mode
## How to test
- Run `./ninja check:complexity` locally and confirm it passes.
- Test diff mode: `just complexipy-diff main`.
## Linked issue
#4918#4949
## Summary
Bump anki-audio to 0.2.1 for the macOS hotfix.
## Steps to reproduce (before)
Audio playback/recording on macOS was broken (see issue).
## How to test (after)
Confirm audio is working now.
## Linked issue
Closes#4836
## Summary/motivation
Three related housekeeping changes to improve AI-agent and developer
ergonomics:
1. **justfile**: Expose `./run`, `./run.bat` (Windows),
`./tools/web-watch`,
`./tools/rebuild-web`, and `./tools/clean` as `just` recipes (`run`,
`run-optimized`, `web-watch`, `rebuild-web`, `clean`), consistent with
the
Project convention that all commands go through `just`.
2. **CLAUDE.md**: Add a "Running Anki" section documenting `just run`
and
`just web-watch`; tighten the opening note to explicitly mention
`./run`.
3. **AGENTS.md**: Add a symlink to `CLAUDE.md` so that OpenAI Codex and
other
Agents that look for `AGENTS.md` pick up the same project instructions.
## How to test
- `just run` launches Anki in dev mode (same as `./run`)
- `just run-optimized` launches with `RELEASE=1`
- `just web-watch` starts the file-watcher (macOS/Linux)
- `just clean` removes build outputs
- `AGENTS.md` resolves to the same content as `CLAUDE.md`
## Linked issue
Closes#4949
## Summary
It turned out that our mpv.rb formula was never used because of the
`bottle do` statement, which was telling brew to use the upstream
version.
## Steps to reproduce (before)
1. Download and extract anki-audio==0.2.1:
https://pypi.org/project/anki-audio/0.2.1/
2. Run `./mpv --version` and confirm you see the error in the linked
issue.
## How to test (after)
1. Extract the signed artifacts built from this PR:
https://github.com/ankitects/anki/actions/runs/26877282940
2. Run `./mpv --version` and confirm no library errors. You might see
Gatekeeper warnings here as the binary is not notarized.
## Linked issue
Closes#4945
## Summary / motivation
Two small clarifications to the contributing guide:
- Added a **Pull Request Description** section pointing contributors to
the
PR template (`.github/pull_request_template.md`), so they know upfront
what fields are required before opening a PR.
- Strengthened the test coverage requirement from "please consider" to a
clear rule, with explicit exceptions (version bumps, docs, translations,
dependency updates, chore).
Fixes#4936
## Problem
The `check-linked-issue` workflow runs on every `opened` and `edited` PR
event. When a PR had no linked issue, each edit triggered a new bot
comment, resulting in duplicates (e.g. #4934).
## Solution
Before posting the comment, list the existing PR comments and skip
posting if the bot has already left one with the same message.
The `missing-issue` label re-application is harmless since GitHub
deduplicates labels automatically.
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
<!-- Fixes#123 / Closes#123 / Refs #123 -->
closes#4722
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
I used this nice script I found:
https://gist.github.com/onnimonni/3462f958c7d235417863651974514525
For the reasons behind this change see:
- #4722
## Steps to reproduce (required, use N/A if not applicable)
<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
- Mainly for bugfixes;
- For bugs: numbered steps before the fix. For non-bugs: write N/A.
- use N/A for features, refactors, docs, chore, etc.
-->
(N/A)
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
See it run in my repo here:
https://github.com/Luc-Mcgrady/anki/actions/runs/26718877866
### Checklist (minimum)
- [X] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [X] This PR is focused on one change (no unrelated edits).
## Linked issue
Closes#4918
## Summary
The anki-audio wheel build script was accidentally using `lib` instead
of `libs` for copied mpv libraries.
## Steps to reproduce (before)
Follow reproduction steps in the forum report and confirm you see the
issue.
## How to test (after)
- Build the wheel: `./qt/audio/build.sh`.
- Confirm `qt/audio/anki_audio/libs` is created.
- Extract `out/wheels/anki_audio-0.2.0-*.whl` and confirm it has the
correct structure.
- Run mpv in the extracted wheel: `./mpv` and confirm no library errors.
## Linked issue
Closes#4920
## Summary
Default to PowerShell 7+ to run just commands on Windows for more
consistent argument quoting rules.
## Steps to reproduce (before)
- Trigger the release workflow: `just release::build --ref main`.
- Inspect the logs of the `prepare` step on GitHub and notice that the
version has literal double quotes.
## How to test (after)
Repeat the same steps and confirm quotes are not passed literally.
## Linked issue
Closes#4699
## Summary
This bumps `anki-audio` to the newly published 0.2.0 release.
## How to test
Confirm audio recording & playback is working by using the record button
in the editor.
## Linked issue
Closes#4816
## Summary / motivation
Adds two workflows to enforce the rule that every PR must be linked to
an existing issue:
- **check-linked-issue**: triggers on PR open/edit, applies the
`missing-issue` label and notifies the author if no linked issue is
found. Removes the label if the author later links one.
- **auto-close-missing-issue**: runs daily and closes any PR that has
had the `missing-issue` label for more than 4 days.
Hotfixes (title contains `hotfix`) and Dependabot PRs are exempt.
## How to test
1. Open a PR without a linked issue, the `missing-issue` label should be
applied and a comment posted.
2. Edit the PR description to add `Closes #<number>`, the label should
be removed.
3. Trigger the auto-close workflow manually via Actions → `Auto-close
PRs without linked issue` → Run workflow, and verify it closes PRs that
have had the label for over 4 days.
## Linked issue
Closes#4908
## Summary / motivation
Update the Wix template to allow users to install older versions of the
app, overwriting an existing newer version. See changes in the template:
f4b00da7d0
## Steps to reproduce (before)
1. Build the installer (`./tools/ninja installer:package`) with the
current main branch.
2. Install the MSI package at out/installer/dist.
3. Remove the out/installer folder, modify the .version file to decrease
the version (e.g. `25.09.1`) and build the installer again.
4. Try to install the package - you should get an error message.
## How to test (after)
Repeat the same process and confirm you can install the older version
with no errors.
## Summary
- Pass `type="rich"` to `showText()` when displaying the sync server
message, enabling HTML rendering via `QTextBrowser.setHtml()`
- Plain-text messages continue to render identically — `setHtml()`
handles plain strings the same as `setPlainText()`
- Allows self-hosted sync servers to send formatted post-sync messages
(e.g. styled statistics, notices with links, tables)
## Context
The `/sync/meta` response includes a `msg` field that is displayed to
the user after sync. The display widget is already a `QTextBrowser`
(which supports Qt rich text/HTML), but `showText()` is called with the
default `type="text"`, routing through `setPlainText()`.
The `showText` helper already has full HTML support — it just needs
`type="rich"`:
```python
# qt/aqt/utils.py
text = QTextBrowser()
text.setOpenExternalLinks(True)
if type == "text":
text.setPlainText(txt) # current path
else:
text.setHtml(txt) # proposed path
```
**Security:** `QTextBrowser` does not execute JavaScript — only static
HTML/CSS. The sync server is explicitly configured and trusted by the
user.
Strip HTML from note type and template names in the Empty Cards screen.
## How to test
```
cargo test -p anki --lib notetype::emptycards
```
---------
Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
## Linked issue
A slight improvement related to #4314
## Summary
Import some modules like `jsonschema`, `bs4` and `aqt.mediasrv` lazily
to speed up startup a bit.
## Steps to reproduce
Run Anki with `-X importtime` passed to Python:
```diff
diff --git a/run b/run
index 3051345b1..a907b2925 100755
--- a/run
+++ b/run
@@ -17,4 +17,4 @@ export ANKI_API_PORT=${ANKI_API_PORT-40000}
export ANKI_API_HOST=${ANKI_API_HOST-127.0.0.1}
./ninja pylib qt
-${PYENV}/bin/python tools/run.py $*
+${PYENV}/bin/python -X importtime tools/run.py 2> importtime.txt $*
diff --git a/run.bat b/run.bat
index aecbf2491..69a721641 100755
--- a/run.bat
+++ b/run.bat
@@ -12,5 +12,5 @@ set ANKI_API_HOST=127.0.0.1
@if not defined PYENV set PYENV=out\pyenv
call tools\ninja pylib qt || exit /b 1
-%PYENV%\Scripts\python tools\run.py %* || exit /b 1
+%PYENV%\Scripts\python -X importtime tools\run.py %* 2> importtime.txt || exit /b 1
popd
```
Then import `importtime.txt` to
[https://github.com/kmichel/python-importtime-graph](https://github.com/kmichel/python-importtime-graph)
to visualize timings. You should see `jsonschema` and `bs4` are loaded
at startup.
## How to test
Run profiling again and confirm `jsonschema` and `bs4` are not loaded at
startup. `aqt.mediasrv` will still be loaded at startup (because it's
immediately used in the main screen) but at a later stage.
---------
Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com>
This just limits the fix added in
469fd763c7
to the launcher so it has no effect on Briefcase builds. It looks
harmless, but just in case it does cause subtle issues.
---------
Co-authored-by: Fernando Lins <1887601+fernandolins@users.noreply.github.com>
## Linked issue
Closes#4896
## Summary
On macOS, clicking the Help button in error dialogs shown on top of
modal dialogs such as the Filtered deck screen was causing the app to
become unresponsive after the recent Qt upgrade. This was fixed by:
1. Setting a parent widget for `QMessageBox`.
2. Limiting the `.disconnect()` calls to the `clicked` signal.
## Steps to reproduce (before)
I was not able to reproduce the issue in a dev environment. I had to
build and run the Briefcase package. Follow the steps in the
[forums](https://forums.ankiweb.net/t/anki-26-05-beta-1/69707/43?u=abdo)
and confirm you can reproduce the issue.
## How to test (after)
Run the Briefcase package with the changes and follow reproduction steps
and confirm the issue is fixed.
## Linked issue
Closes#4834
## Summary
This installs required Linux system dependencies in the install.sh
script for Debian-based distributions.
## Steps to reproduce (before)
No system dependencies are installed automatically. Users have to refer
to the
[manual](https://docs.ankiweb.net/platform/linux/installing.html#requirements)
to find a (partial) list of required packages.
## How to test (after)
Build the installer and run install.sh in any supported distro and
confirm dependencies are installed.
## Linked issue
Closes#4874
## Summary / motivation
Adds `tools/coverage/check-coverage-regression.py` to compare line
coverage percentages from the current PR against the baseline saved from
main (introduced in #4875). If any stack regresses beyond the
configured tolerance (0.10%), the CI fails with a clear message showing
the delta.
Stacks checked: Rust, python-pylib, python-qt, TypeScript.
## How to test
Try to add some new code without any tests. The Ci must fail.
## Before / after behavior
Before: no signal when a PR reduces coverage below the current main
level.
After: CI fails on `Check coverage regression` with output like:
```
[rust] REGRESSION: 62.64% -> 61.00% (delta: -1.64%, tolerance: 0.10%)
1 stack(s) with coverage regression: rust
```
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4433
- #4716
## Summary / motivation (required)
This PR modifies `Check Media` to dedupe filenames case-insensitively so
as to avoid files that were added on case-sensitive filesystems
potentially being overwritten on case-insensitive ones
## Steps to reproduce (required, use N/A if not applicable)
N/A
## How to test (required)
Given a.jpg (hash 1), A.JPG (hash 2) and a.JPG (hash 1) as existing
media files on a case-sensitive fs, see that `Check Media` renames A.JPG
to a-2.jpg and a.JPG to a.jpg
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4716
## Summary / motivation (required)
Currently, on filesystems where the media folder is treated as
case-insensitive, `[sound:BLAH.mp3]` and `[sound:blah.mp3]` point to the
same file on disk (if any), but one of the two would be considered as a
ref pointing to a missing file by the media checker which assumes
case-sensitivity
## Steps to reproduce (required, use N/A if not applicable)
See linked pr
## How to test (required)
On windows/macos, add `blah.mp3`, rename `blah.mp3` to `Blah.mp3` in the
ref, run `Check Media` and see that `Blah.mp3` isn't considered missing
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
Use Python logging facilities instead of printing directly to stdout
with print(). This prevents library consumers from being spammed with
unwanted debug logs and stack traces.
Closes#4665
---
My approach to the log levels is `warning` for any deprecation warning
and `debug` for the "blocked main thread" messages.
The pylib is used in the qt parts of the anki code. To the best of my
understanding, logging is already correctly set up there and no
adjustments are needed.
The [issue I created](https://github.com/ankitects/anki/issues/4665) for
this has a reproducer. With this change applied, the messages are gone
when configuring the anki logger accordingly.
```python3
from anki.collection import Collection
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("anki").setLevel(logging.INFO)
col = Collection("collection.anki2")
auth = col.sync_login(
username=r"<username>",
password=r"<password>",
endpoint="https://sync.ankiweb.net/",
)
col.close()
```
setting the anki logger back to `DEBUG` makes the message appear again.
Note that the format is only slightly different then previously.
```txt
DEBUG:anki._backend:blocked main thread for 309ms
Stack (most recent call last):
File "/home/david/coding_stuff/anki/repro.py", line 8, in <module>
auth = col.sync_login(
File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/collection.py", line 1141, in sync_login
return self._backend.sync_login(
File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/_backend_generated.py", line 83, in sync_login
raw_bytes = self._run_command(1, 3, message.SerializeToString())
File "/home/david/coding_stuff/anki/.venv/lib/python3.14/site-packages/anki/_backend.py", line 168, in _run_command
logger.debug(
```
## Linked issue
Closes#4873
## Summary
Build and package the fcitx5-qt6 plugin.
Latest release CI run:
https://github.com/ankitects/anki/actions/runs/26294296416
## How to test
1. Run installer build on Linux: `./ninja installer:build`.
2. Go to the Qt build directory
(`out/installer/build/anki/linux/zip/anki/app_packages/PyQt6/Qt6`) and
confirm you see the following files:
1.
`plugins/platforminputcontexts/libfcitx5platforminputcontextplugin.so`
2. `plugins/dbusaddons/libFcitx5Qt6DBusAddons.so*`
## Linked issue
Refs #4874
## Summary / motivation
Stores the coverage results from every push to `main` in a GitHub
Actions
cache (`coverage-baseline-linux-{sha}`). This is the foundation for a
follow-up PR that will compare PR coverage against this baseline and
fail
if any stack regresses.
No behavior change for PRs yet — the baseline is only saved, not used.
## Before / after behavior
Before: no coverage data persisted between CI runs.
After: each push to `main` saves `out/coverage/` as a cache entry, keyed
by commit SHA, retrievable by prefix `coverage-baseline-linux-`.
## Linked issue
Closes#4863
## Summary / motivation
Adds Playwright as the e2e test framework so contributors can write
browser-based tests against a real headless Anki instance. There was no
automated way to exercise mediasrv pages, SvelteKit routes, or the
`/_anki/` RPC surface from a browser, this PR establishes that harness.
Key pieces:
- `qt/tests/launch_anki_for_e2e.py` — spawns a throwaway Anki instance
(temp `ANKI_BASE`, `QT_QPA_PLATFORM=offscreen`). Pre-seeds `prefs21.db`
so Anki skips the language picker and profile chooser and goes straight
to serving mediasrv.
- `playwright.config.ts` — points `webServer` at the launcher; polls
`/favicon.ico` as the readiness probe.
- `ts/tests/e2e/` — `fixtures.ts` base and a sanity spec that verifies
mediasrv is reachable and a SvelteKit page hydrates.
- `justfile` — `just test-e2e` recipe; Chromium installed to
`out/playwright-browsers/`.
- CI — e2e step in `check-linux`; failed-run artifacts uploaded for 7
days.
- `docs/e2e-testing.md` — contributor guide covering setup, managed vs
reuse-server modes, and writing new tests.
## How to test
Build the project once, then run the e2e suite in managed mode (no
separate `./run` needed — the launcher is started automatically):
```shell
just build
just test-e2e
```
## Before / after behavior (optional)
Before: no browser-level test harness existed.
After: `just test-e2e` drives a real headless Anki instance via
Playwright.
## Risk / compatibility / migration
No production code changed. New dev-only files and CI step only.
Chromium is installed to `out/playwright-browsers/` (gitignored) and
does not affect the regular build.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Closes#4859
## Summary
Add tests for the build_installer.py script with 100% coverage.
## How to test
Run `just test-py --coverage --html` and browse coverage data.
## Linked issue
Closes#4870
## Summary
This fixes the border color of browser cells in macOS not following
Anki's theme after Qt 6.10+
## Steps to reproduce (before)
1. Set Anki's theme from _Anki (`python` in dev environment) >
Preferences > Theme_ to be the opposite of your system theme.
2. Open the Browse screen and notice the cell borders have the opposite
color of your Anki theme, matching the system theme.
## How to test (after)
Notice cell borders are more subtle now.
### Details
## UI evidence
Before:
<img width="628" height="346" alt="image"
src="https://github.com/user-attachments/assets/3b46c9fe-5eb8-4e03-ac93-5429d1768344"
/>
After:
<img width="628" height="346" alt="image"
src="https://github.com/user-attachments/assets/aeea6867-ebfe-46cc-8b4a-6f7440da16f4"
/>
## Linked issue
Refs #4838#4839#4840
## Summary / motivation
Adds a contributor-facing guide for the test coverage setup introduced
across the three coverage PRs, and updates CLAUDE.md to reflect that
`just` is now the single entry point for all build, test, lint, and
format commands.
## How to test
- Read `docs/testing-coverage.md` — verify it covers all three stacks,
thresholds, and the known gaps section.
- Run `just docs` and open `out/docs/html/index.html` to confirm
`testing-coverage` appears in the sidebar under Contributing.
- Read `CLAUDE.md` — verify `./ninja` / `./tools` references are gone
and `just test-rust`, `just test-py`, `just test-ts` are mentioned.
### Details
- `docs/testing-coverage.md`: documents tools, thresholds, per-stack
entry points, and known gaps for all three coverage stacks.
- `docs/index.md`: wired `testing-coverage` into the Sphinx toctree
after `contributing`.
- `CLAUDE.md`: replaced raw `./ninja` / `./tools` invocations with
`just` equivalents; added top-level note directing contributors to
`just --list`; added `just test-rust`, `just test-py`, `just test-ts`
to the Quick iteration section.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
See
https://github.com/ankitects/anki/issues/4314#issuecomment-3289709713
## Summary
We got reports that certifi's cacert.pem generates a lot of IO events at
startup. This file is no longer directly used by Anki, as we use
truststore to force libraries such as requests to use system stores, but
it's apparently being loaded by requests in any case.
## How to test
Run tests: `./ninja check:pytest`
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
- #4851
## Summary / motivation (required)
I made a mistake in #4851 by not lowercasing in the case where a file
with the same name but different contents already exists, leading to
inconsistent behaviour across platforms
## Steps to reproduce (required, use N/A if not applicable)
See linked pr
## How to test (required)
Run ./check on linux and windows and see that the same lowercasing
behaviour occurs on both
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
Fixes#4716
## Summary / motivation (required)
#4435 made it so that all new added media would have lowercased names,
but this was bugged and led to media refs pointing to inexistent files
on case-insensitive filesystems
The fix proposed is to try adding new files with lowercased names only
if they don't already exist, using the existing file otherwise
## Steps to reproduce (required, use N/A if not applicable)
See linked issue
## How to test (required)
Try copy-pasting a media file from the Edit window of an existing note
to the Add window and see that the filename in the resulting media ref
isn't forced to be lowercased
When pasting a new file, see that the filename is now lowercased
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [ ] I added or updated tests when the change is non-trivial or
behavior changed.
## Scope
- [x] This PR is focused on one change (no unrelated edits).
## Linked issue
Closes#4840
## Summary / motivation
Adds Vitest V8 coverage for TypeScript/Svelte tests via
`@vitest/coverage-v8`.
Introduces `just test-ts --coverage` and `just test-ts --coverage
--html`,
and wires TypeScript into the `just test --coverage` umbrella —
completing
coverage support across all three stacks (Python, Rust, TypeScript).
The threshold is set to 5% — intentionally low because the Vitest test
count is small relative to the TypeScript/Svelte source surface. It is
meant to be raised as more tests are added.
## How to test
```sh
# Existing behavior unchanged
just test-ts
# Terminal summary + enforces 5% line coverage threshold
just test-ts --coverage
# Terminal summary + HTML report under out/coverage/typescript/
just test-ts --coverage --html
# Full umbrella — all three stacks
just test --coverage
just test --coverage --html
```
### Checklist
- [x] I ran `./ninja check` or an equivalent relevant check locally.
### Details
- `@vitest/coverage-v8` pinned at `3.2.4` in `package.json`.
- Reports are written to `out/coverage/typescript/` via
`--coverage.reportsDirectory=../out/coverage/typescript` (relative to
the `ts/` working directory where vitest runs).
- V8 provider is preferred over Istanbul: faster and requires no Babel
transform for TypeScript projects.
- Coverage measures only code reachable through Vitest's module graph —
Svelte component rendering is not covered.
- The `yarn` justfile variable is added for platform-aware yarn
invocation (Windows vs Unix).
## Before / after behavior
Before: no `just test-ts`, no TypeScript coverage.
After: `just test-ts` runs Vitest via ninja; `just test-ts --coverage`
runs with V8 instrumentation.
`just test --coverage` now spans all three stacks.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
#4855
## Summary
Compile .pyc files and package them in the Briefcase bundle instead of
the .py sources.
## How to test
You can run this Powershell script to do a basic benchmark of startup
performance on Windows (Requires
[py-spy](https://github.com/benfred/py-spy)):
```powershell
function Remove-Build {
Remove-Item -LiteralPath "./out/installer" -Force -Recurse
}
function Start-Anki {
param (
[Parameter()]
[string]
$OutputPath
)
Start-Process -FilePath "py-spy" -ArgumentList "record --output $OutputPath -- ./out/installer/build/anki/windows/app/src/Anki.exe --safemode"
Start-Sleep -Seconds 10
Stop-Process -Name Anki
}
if ($null -eq (Get-Command "py-spy" -ErrorAction SilentlyContinue)) {
Write-Error "py-spy not found. See https://github.com/benfred/py-spy"
Exit 1
}
git checkout main
Remove-Build
./tools/ninja installer:build
Start-Anki -OutputPath "main.svg"
git checkout briefcase-compile-pyc
Remove-Build
./tools/ninja installer:build
Start-Anki -OutputPath "pyc.svg"
```
Output is written to `main.svg` and `pyc.svg`. Here's an example run
(using default 100 sampling rate):
<img width="950" height="559" alt="image"
src="https://github.com/user-attachments/assets/5da6a06a-3393-4f0e-80fe-ced735d50c2c"
/>
<img width="830" height="352" alt="image"
src="https://github.com/user-attachments/assets/ca9e5d8d-c3d4-4a0b-bd86-7aa2d6c5cee2"
/>
## Linked issue
Closes#4839
## Summary / motivation
Adds `cargo-llvm-cov`-based test coverage for the full Rust workspace.
Introduces `just test-rust --coverage` and `just test-rust --coverage
--html`, and wires Rust into the `just test --coverage` umbrella.
`cargo-llvm-cov` is installed on demand into `out/bin/` to avoid
polluting the global cargo install. The `llvm-tools-preview` rustup
component is now installed in CI so the tool can instrument binaries.
## How to test (required)
```sh
# Existing behavior unchanged
just test-rust
# Terminal summary
just test-rust --coverage
# Terminal summary + HTML report under out/coverage/rust/html/
just test-rust --coverage --html
# Umbrella (Rust + Python)
just test --coverage
just test --coverage --html
```
Note: first run of `--coverage` will install `cargo-llvm-cov` into
`out/bin/` (~30s). Subsequent runs skip the install step.
### Checklist
- [x] I ran `./ninja check` or an equivalent relevant check locally.
### Details
- `cargo-llvm-cov` pinned at `0.8.4`, installed into `out/bin/` via
`cargo install --root out`.
- `--workspace --locked` measures all crates and respects the lockfile.
- `llvm-tools-preview` added to `setup-anki` action so CI can instrument
Rust binaries.
- Coverage runs are slower than plain `just test-rust` because
`cargo-llvm-cov` rebuilds with instrumentation — this is expected.
## Before / after behavior
Before: no `just test-rust`, no Rust coverage support.
After: `just test-rust` runs Rust tests via ninja; `just test-rust
--coverage` runs them with `cargo-llvm-cov`
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue (required)
Fixes#4833
## Summary / motivation (required)
Keep the Browser window focused after exporting selected notes.
The export dialog was opened from the Browser, but the background export
progress and completion tooltip were still parented to the main window.
That could leave the main screen focused after the export completed.
This change carries the dialog parent through the export options and
uses it for progress, errors, and the completion tooltip.
## Steps to reproduce (required, use N/A if not applicable)
1. Open the Browser.
2. Right-click a note and choose Notes > Export Notes.
3. Click Export, choose a save location, and save.
4. Observe that the main screen receives focus after export completion.
## How to test (required)
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
- Ran `./ninja check` successfully.
- Tested behavior on macOS, however I currently don't have a Windows or
Linux environment set up to test on right now as I'm on vacation. The
change is limited to dialog parenting, so I expect it to work the same
across platforms. Would appreciate testing on other platforms if
possible.
## Before / after behavior (optional)
Before: exporting selected notes from the Browser could focus the main
window after completion.
After: export progress and completion UI remain parented to the Browser,
so focus is returned to the Browser.
## Risk / compatibility / migration (optional)
Low risk. The change is limited to export UI parenting and falls back to
the main window when no dialog parent is available.
## UI evidence (required for visual changes; otherwise N/A)
N/A
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue
Closes#4847
## Summary
Prevent Windows elevation errors when running ninja_gen's update
binaries by embedding a manifest with `asInvoker`.
## How to test
Run `./tools/ninja check` or `cargo run --bin update_node` and confirm
no elevation errors.
## Linked issue
Closes#4838
## Summary/motivation
Adds `coverage.py`-based test coverage for both Python test suites
(`pylib` and `qt`). Introduces `just test-py --coverage` and `just
test-py --coverage --html`, plus the `just test --coverage`.
Coverage reports are written to `out/coverage/`.
## How to test
```sh
# Existing behavior unchanged
just test-py
# Terminal summary + enforces thresholds
just test-py --coverage
# Terminal summary + HTML reports under out/coverage/
just test-py --coverage --html
# Umbrella (Python only for now)
just test --coverage
just test --coverage --html
```
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
### Details
- `coverage` dependency pinned to >=7.13.5 in pyproject.toml.
- The `coverage` umbrella recipe currently delegates to Python only for
now
## Before / after behavior
Before: no `just test-py`, no coverage support.
After: `just test-py` runs Python tests via ninja; `just test-py
--coverage`
runs them with `coverage.py` and enforces minimum line coverage.
---------
Co-authored-by: Abdo <abdo@abdnh.net>
## Linked issue (required)
[[<!-- Fixes#123 / Closes#123 / Refs #123
-->](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)
## Summary / motivation (required)
See discussion about this on [the
forum](https://forums.ankiweb.net/t/anki-collection-collection-export-anki-package-crashes-if-given-a-relative-path-with-one-component/69562)
## Steps to reproduce (required, use N/A if not applicable)
`demo.py`:
```python
import tempfile
from pathlib import Path
import anki.collection
with tempfile.TemporaryDirectory() as tempdir:
tempdir = Path(tempdir)
col = anki.collection.Collection(str(tempdir / "temp.anki2"))
col.export_anki_package(
out_path="output.apkg",
options=anki.collection.ExportAnkiPackageOptions(),
limit=None,
)
```
```console
$ python demo.py
Traceback (most recent call last):
[... some noise elided here ...]
File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend_generated.py", line 2037, in export_anki_package
raw_bytes = self._run_command(37, 4, message.SerializeToString())
File "/nix/store/1w7swjzmjp7171yspih1q07a9vgacjzb-dev-env/lib/python3.14/site-packages/anki/_backend.py", line 171, in _run_command
raise backend_exception_to_pylib(err)
anki.errors.BackendIOError: Failed to open '': No such file or directory (os error 2)
```
## How to test (required)
I added a unit test. It passes.
### Checklist (minimum)
- [ ] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
<!-- Commands, manual steps, edge cases, and what you observed -->
## Before / after behavior (optional)
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>
<!--
Title (for the Pull Request title field at the top):
Use a short prefix so the change type is obvious. You do not need to
repeat it in the body below.
Examples:
- fix: — bugfix
- feat: — feature
- refactor: — internal change without user-facing feature
- docs: — documentation only
- chore: — tooling, CI, deps, build housekeeping
- test: — tests only
-->
## Linked issue (required)
Fixes#4835
## Summary / motivation (required)
<!-- What this PR does and why. For larger changes, add enough context
for reviewers. -->
Fixes the issue #4835 with a minor change in InvalidRegex error.
## Steps to reproduce (required, use N/A if not applicable)
<!-- Steps to reproduce: how to trigger the bug in the broken state (the
"before").
- Mainly for bugfixes;
- For bugs: numbered steps before the fix. For non-bugs: write N/A.
- use N/A for features, refactors, docs, chore, etc.
-->
1. Open the browser and do a find & replace.
2. Put b[ in “Find”.
3. Enable the regex option.
4. Confirm.
## How to test (required)
<!--- How to test: how you verified the change (checks, unit tests,
manual steps, edge cases — the "after" or general validation). --->
Reproduce the fix and verify the regex error is not within the `<pre>`
tag.
### Checklist (minimum)
- [x] I ran `./ninja check` or an equivalent relevant check locally.
- [x] I added or updated tests when the change is non-trivial or
behavior changed.
### Details
Regex error was return with `<pre>` tags which are removed now so that
the text in the error box is displayed correctly.
## Before / after behavior (optional)
<img width="372" height="211" alt="image"
src="https://github.com/user-attachments/assets/91f53745-301b-4679-b1a5-53fafd628de7"
/>
<!-- For bugfixes: behavior before vs after. For other types: N/A or a
short note. -->
## Risk / compatibility / migration (optional)
<!-- Breaking changes, rollout notes, or N/A for small / low-risk PRs
-->
## UI evidence (required for visual changes; otherwise N/A)
<img width="360" height="226" alt="image"
src="https://github.com/user-attachments/assets/3b7b3f23-9f35-423e-9b10-62834e0a0dd6"
/>
<!-- Screenshot or short video -->
## Scope
- [x] This PR is focused on one change (no unrelated edits).
---------
Co-authored-by: Abdo <abdo@abdnh.net>