Commit Graph

12279 Commits

Author SHA1 Message Date
dependabot[bot]
b49e21163b chore(deps): bump gitpython from 3.1.49 to 3.1.50
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.49 to 3.1.50.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.49...3.1.50)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.1.50
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 12:59:31 +00:00
Abdo
8034ebc160 chore: Tweak gh release name (#4964)
Remove "Anki" from the titles of releases.
2026-06-08 15:58:20 +03:00
Fernando Lins
110cb8f3b1 fix(sass): replace global map-get/map-merge with sass:map namespace in _functions.scss (#4955)
## 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"
/>
2026-06-05 09:25:14 -03:00
Fernando Lins
067d04f4ab chore(sass): remove unused Bootstrap imports from _button-mixins.scss (#4953)
<!--
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"
/>
2026-06-05 09:24:36 -03:00
Abdo
f5d9b9ef3c chore: Integrate Complexipy for complexity analysis (#4942)
## 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`.
2026-06-04 19:29:37 +03:00
Fernando Lins
da64f03307 chore(deps): consolidated security updates (Dependabot batch) (#4934)
## Summary

Consolidates 8 open Dependabot PRs into a single security-focused batch.

### Addressed advisories

- GHSA: tar (Cargo)
[#296](https://github.com/ankitects/anki/security/dependabot/296)
- @tootallnate/once
[#295](https://github.com/ankitects/anki/security/dependabot/295)
- ws [#293](https://github.com/ankitects/anki/security/dependabot/293)
- openssl
[#292](https://github.com/ankitects/anki/security/dependabot/292)
- idna [#291](https://github.com/ankitects/anki/security/dependabot/291)
- devalue
[#287](https://github.com/ankitects/anki/security/dependabot/287)/[#239](https://github.com/ankitects/anki/security/dependabot/239)/[#217](https://github.com/ankitects/anki/security/dependabot/217)/[#216](https://github.com/ankitects/anki/security/dependabot/216)
- postcss
[#275](https://github.com/ankitects/anki/security/dependabot/275)
- svelte
[#286](https://github.com/ankitects/anki/security/dependabot/286)/[#288](https://github.com/ankitects/anki/security/dependabot/288)/[#289](https://github.com/ankitects/anki/security/dependabot/289)
- lodash-es
[#258](https://github.com/ankitects/anki/security/dependabot/258)/[#259](https://github.com/ankitects/anki/security/dependabot/259)
- @sveltejs/kit
[#294](https://github.com/ankitects/anki/security/dependabot/294)

### Sources

Merged from PRs: #4914, #4887, #4867, #4866, #4865, #4846, #4744, #4892.

### Not addressed — rand (Cargo)
[#268](https://github.com/ankitects/anki/security/dependabot/268)

PR #4741 (rand 0.9.4 → 0.10.1) was excluded because `fsrs 5.2.0` still
depends on `rand 0.9.4`. The rand 0.10 API changes (`Rng` → `RngExt`)
cause a compile error at the `PostSchedulingFn` boundary. This will be
unblocked when fsrs is upgraded.

### Not addressed (transitive — follow-up)

- urllib3
[#284](https://github.com/ankitects/anki/security/dependabot/284)/[#283](https://github.com/ankitects/anki/security/dependabot/283)
- GitPython
[#282](https://github.com/ankitects/anki/security/dependabot/282)
- ip-address
[#276](https://github.com/ankitects/anki/security/dependabot/276)
- pytest
[#266](https://github.com/ankitects/anki/security/dependabot/266)
- Pygments
[#256](https://github.com/ankitects/anki/security/dependabot/256)
- brace-expansion
[#255](https://github.com/ankitects/anki/security/dependabot/255)/[#158](https://github.com/ankitects/anki/security/dependabot/158)
- picomatch
[#253](https://github.com/ankitects/anki/security/dependabot/253)/[#252](https://github.com/ankitects/anki/security/dependabot/252)
- tar (npm)
[#238](https://github.com/ankitects/anki/security/dependabot/238)/[#235](https://github.com/ankitects/anki/security/dependabot/235)/[#209](https://github.com/ankitects/anki/security/dependabot/209)
- immutable
[#231](https://github.com/ankitects/anki/security/dependabot/231)
- minimatch
[#227](https://github.com/ankitects/anki/security/dependabot/227)/[#226](https://github.com/ankitects/anki/security/dependabot/226)/[#221](https://github.com/ankitects/anki/security/dependabot/221)
- fabric
[#211](https://github.com/ankitects/anki/security/dependabot/211)

These need manual `yarn.lock` resolutions / `uv.lock` overrides.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-03 15:10:39 -03:00
Abdo
0073124811 chore: Bump anki-audio to 0.2.1 (#4947)
## 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.
2026-06-03 19:03:36 +03:00
Fernando Lins
935cabbcf6 chore: Updates just recipes and add AGENTS.md (#4943)
## 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`
2026-06-03 12:02:18 -03:00
Abdo
27f8e8c263 fix: custom mpv.rb formula not being used (#4950)
## 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.
2026-06-03 18:00:56 +03:00
Fernando Lins
a62e6f7370 docs: expand contributing guide with PR process and test requirements (#4946)
## 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).
2026-06-02 15:09:47 -03:00
Fernando Lins
c71c13b56e fix: prevent duplicate "missing linked issue" comments on PR edits (#4937)
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.
2026-06-02 15:09:20 -03:00
Luc Mcgrady
fc5103bc33 chore(ci):Use commit SHAs for github actions (#4916)
<!--
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).
2026-06-02 10:35:32 -03:00
Abdo
398b51b2ed fix: wrong path of mpv libs in anki-audio wheel (#4919)
## 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.
2026-06-02 12:30:45 +03:00
Abdo
2f3cff1b38 chore: Exclude github-actions[bot] from contributor check (#4923)
Closes #4922
2026-06-01 16:50:06 +03:00
Abdo
743b6bbb0d fix: Use Powershell to run just commands on Windows (#4921)
## 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.
2026-06-01 16:49:39 +03:00
Abdo
1822a7c76c feat: Bump anki-audio package (#4913)
## 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.
2026-05-29 22:52:19 +03:00
Abdo
60dcc5f3c5 fix: Update GitHub environments (#4912)
## Linked issue

Closes #4911

## Summary

Update release/publish workflows to use the new environments.
2026-05-29 20:26:32 +03:00
Fernando Lins
4702443f31 ci: enforce linked issue requirement on PRs (#4910)
## 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.
2026-05-29 14:24:42 -03:00
Abdo
e5ea3fb40a feat: Allow MSI installer downgrades (#4909)
## 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.
2026-05-28 18:43:37 +03:00
Abdo
5e46fc4494 fix: Validate add-on's zip entries (#4901)
This validates add-on's zip paths to skip things such as UNC paths
2026-05-27 16:30:06 +03:00
Abdo
56d93bb3da fix: Parameterize inputs of prop:cd* queries (#4905)
It was possible to construct invalid SQLite queries for browser searches
in some cases, for example by searching for `prop:cds:foo=';`.
2026-05-27 16:29:42 +03:00
Tim Arnold
9c72a8b828 feat: render sync server messages as rich text (#4560)
## 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.
2026-05-26 21:59:23 +03:00
Abdo
5d1266eb8e fix: Strip HTML from note type names in Empty Cards (#4902)
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>
2026-05-26 18:55:43 +03:00
Abdo
6f332d3a6b refactor: import some modules lazily (#4831)
## 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>
2026-05-26 18:51:31 +03:00
Abdo
88ef75f363 fix: Limit taskbar pinning fix to launcher builds (#4849)
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>
2026-05-26 13:22:18 +03:00
Abdo
6d42e37b72 fix: app unresponsive after clicking on Help button in modal dialogs (#4897)
## 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.
2026-05-26 13:03:02 +03:00
Abdo
892aa9c7a9 fix: Avoid broad warnings filter (#4900)
This replaces a broad `warnings.filterwarnings("ignore")` call triggered
when `ANKI_NOVERIFYSSL` is set.
2026-05-26 13:00:49 +03:00
Abdo
e05a7aace7 feat: Install required system dependencies in install.sh (#4895)
## 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.
2026-05-26 13:00:37 +03:00
Fernando Lins
3f6378aee7 ci(coverage): fail PR if line coverage regresses (#4876)
## 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
```
2026-05-25 13:46:57 -03:00
llama
e9830169b7 feat(check-media): case-insensitively dedupe media filenames (#4852)
<!--
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).
2026-05-25 14:05:34 +03:00
llama
57e67f8408 fix(check-media): case-sensitivity in unused/missing check (#4861)
<!--
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).
2026-05-25 12:09:14 +03:00
David Sauerwein
e13092ef74 Replace print() logging in pylib with Python logging (#4666)
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(
```
2026-05-25 10:55:54 +03:00
Abdo
a754a9c847 feat: Bundle Fcitx plugin (#4886)
## 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*`
2026-05-22 22:33:36 +03:00
Fernando Lins
3ca006dd47 chore(CI): cache coverage baseline from main for regression checks (#4875)
## 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-`.
2026-05-22 16:01:11 -03:00
Fernando Lins
a140d39329 chore(e2e): add Playwright end-to-end test infrastructure (#4864)
## 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>
2026-05-22 15:59:42 -03:00
Abdo
1394540217 test: Add tests for build_installer.py (#4868)
## 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.
2026-05-21 07:10:27 +03:00
Abdo
0519275682 fix: border color of browser cells in macOS (#4872)
## 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"
/>
2026-05-21 07:10:11 +03:00
Fernando Lins
c1b824258e feat: add test coverage docs (#4844)
## 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>
2026-05-20 16:21:23 -03:00
Abdo
bb13a2bb69 fix: Avoid loading unused cacert.pem (#4858)
## 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`
2026-05-20 06:46:41 +03:00
llama
dfbd7d2399 fix(import): force lowercasing before adding hash suffix as well (#4860)
<!--
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).
2026-05-19 06:58:47 +03:00
llama
3479499569 fix(import): avoid case-folding already existing media filenames (#4851)
<!--
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).
2026-05-18 18:56:54 +03:00
Fernando Lins
f9a5b36c82 feat: add TS test coverage (#4843)
## 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>
2026-05-18 11:54:16 -03:00
Abdo
c9cd85dd97 feat: Distribute compiled sources in Briefcase bundle (#4856)
## 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"
/>
2026-05-18 17:47:03 +03:00
Fernando Lins
f76fcec48f feat: add Rust test coverage (#4842)
## 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>
2026-05-18 11:38:37 -03:00
Abdo
03b9fc4814 ci: Fix Windows ARM packaging failing (#4857)
Fixes a CI failure introduced in #4822

See failing run:
https://github.com/ankitects/anki/actions/runs/26009152225/job/76447663427
2026-05-18 17:13:56 +03:00
Tim Gatzke
82eeda9d4f fix(export): preserve browser focus after note export (#4845)
## 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>
2026-05-18 05:30:57 +03:00
Abdo
4b01f78aa3 fix: Prevent Windows elevation errors when running ninja_gen's update binaries (#4848)
## 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.
2026-05-16 11:55:11 +03:00
Fernando Lins
2ea8e5731a feat: add Python test coverage (#4841)
## 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>
2026-05-15 21:34:38 -03:00
Jeremy Fleischman
1482fb0937 fix: atomic_rename now works with single component relative paths (#4773)
## 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>
2026-05-15 16:53:17 +03:00
chiragjagga
a2262809c4 fix: regex error fix (#4837)
<!--
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>
2026-05-15 16:45:41 +03:00