Repository: ankitects/anki
Branch: main
Commit: 2d44d4d6bc48
Files: 1595
Total size: 6.2 MB
Directory structure:
gitextract_gx96pb7w/
├── .buildkite/
│ ├── linux/
│ │ ├── docker/
│ │ │ ├── Dockerfile
│ │ │ ├── build.sh
│ │ │ ├── buildkite.cfg
│ │ │ ├── common.inc
│ │ │ ├── environment
│ │ │ └── run.sh
│ │ ├── entrypoint
│ │ └── release-entrypoint
│ ├── mac/
│ │ └── entrypoint
│ └── windows/
│ └── entrypoint.bat
├── .cargo/
│ └── config.toml
├── .config/
│ └── nextest.toml
├── .cursor/
│ └── rules/
│ ├── building.md
│ └── i18n.md
├── .deny.toml
├── .dockerignore
├── .dprint.json
├── .eslintrc.cjs
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.md
│ │ └── config.yml
│ ├── actions/
│ │ └── setup-anki/
│ │ └── action.yml
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .gitmodules
├── .idea.dist/
│ └── repo.iml
├── .mypy.ini
├── .prettierrc
├── .python-version
├── .ruff.toml
├── .rustfmt-empty.toml
├── .rustfmt.toml
├── .version
├── .vscode.dist/
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── .yarnrc.yml
├── CLAUDE.md
├── CONTRIBUTORS
├── Cargo.toml
├── LICENSE
├── README.md
├── SECURITY.md
├── build/
│ ├── configure/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── aqt.rs
│ │ ├── launcher.rs
│ │ ├── main.rs
│ │ ├── platform.rs
│ │ ├── pylib.rs
│ │ ├── python.rs
│ │ ├── rust.rs
│ │ └── web.rs
│ ├── ninja_gen/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── action.rs
│ │ ├── archives.rs
│ │ ├── bin/
│ │ │ ├── update_node.rs
│ │ │ ├── update_protoc.rs
│ │ │ └── update_uv.rs
│ │ ├── build.rs
│ │ ├── cargo.rs
│ │ ├── command.rs
│ │ ├── configure.rs
│ │ ├── copy.rs
│ │ ├── git.rs
│ │ ├── hash.rs
│ │ ├── input.rs
│ │ ├── lib.rs
│ │ ├── node.rs
│ │ ├── protobuf.rs
│ │ ├── python.rs
│ │ ├── render.rs
│ │ ├── rsync.rs
│ │ └── sass.rs
│ └── runner/
│ ├── Cargo.toml
│ ├── build.rs
│ └── src/
│ ├── archive.rs
│ ├── build.rs
│ ├── main.rs
│ ├── paths.rs
│ ├── pyenv.rs
│ ├── rsync.rs
│ ├── run.rs
│ └── yarn.rs
├── cargo/
│ ├── README.md
│ ├── format/
│ │ └── rust-toolchain.toml
│ └── licenses.json
├── check
├── docs/
│ ├── architecture.md
│ ├── build.md
│ ├── contributing.md
│ ├── development.md
│ ├── docker/
│ │ ├── Dockerfile
│ │ └── README.md
│ ├── editing.md
│ ├── language_bridge.md
│ ├── linux.md
│ ├── mac.md
│ ├── ninja.md
│ ├── protobuf.md
│ ├── syncserver/
│ │ ├── Dockerfile
│ │ ├── Dockerfile.distroless
│ │ ├── README.md
│ │ └── entrypoint.sh
│ └── windows.md
├── ftl/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── copy-core-string.sh
│ ├── core/
│ │ ├── actions.ftl
│ │ ├── adding.ftl
│ │ ├── browsing.ftl
│ │ ├── card-stats.ftl
│ │ ├── card-template-rendering.ftl
│ │ ├── card-templates.ftl
│ │ ├── change-notetype.ftl
│ │ ├── custom-study.ftl
│ │ ├── database-check.ftl
│ │ ├── deck-config.ftl
│ │ ├── decks.ftl
│ │ ├── editing.ftl
│ │ ├── empty-cards.ftl
│ │ ├── errors.ftl
│ │ ├── exporting.ftl
│ │ ├── fields.ftl
│ │ ├── findreplace.ftl
│ │ ├── help.ftl
│ │ ├── importing.ftl
│ │ ├── keyboard.ftl
│ │ ├── launcher.ftl
│ │ ├── media-check.ftl
│ │ ├── media.ftl
│ │ ├── network.ftl
│ │ ├── notetypes.ftl
│ │ ├── preferences.ftl
│ │ ├── profiles.ftl
│ │ ├── scheduling.ftl
│ │ ├── search.ftl
│ │ ├── statistics.ftl
│ │ ├── studying.ftl
│ │ ├── sync.ftl
│ │ └── undo.ftl
│ ├── ftl
│ ├── move-from-ankimobile
│ ├── qt/
│ │ ├── about.ftl
│ │ ├── addons.ftl
│ │ ├── errors.ftl
│ │ ├── preferences.ftl
│ │ ├── profiles.ftl
│ │ ├── qt-accel.ftl
│ │ └── qt-misc.ftl
│ ├── remove-unused.sh
│ ├── src/
│ │ ├── garbage_collection.rs
│ │ ├── main.rs
│ │ ├── serialize.rs
│ │ ├── string/
│ │ │ ├── copy.rs
│ │ │ ├── mod.rs
│ │ │ └── transform.rs
│ │ └── sync.rs
│ ├── update-ankidroid-usage.sh
│ ├── update-ankimobile-usage.sh
│ └── usage/
│ └── no-deprecate.json
├── justfile
├── ninja
├── package.json
├── pkgkey.asc
├── proto/
│ ├── .clang-format
│ ├── .top_level
│ ├── README.md
│ └── anki/
│ ├── ankidroid.proto
│ ├── ankihub.proto
│ ├── ankiweb.proto
│ ├── backend.proto
│ ├── card_rendering.proto
│ ├── cards.proto
│ ├── collection.proto
│ ├── config.proto
│ ├── deck_config.proto
│ ├── decks.proto
│ ├── frontend.proto
│ ├── generic.proto
│ ├── i18n.proto
│ ├── image_occlusion.proto
│ ├── import_export.proto
│ ├── links.proto
│ ├── media.proto
│ ├── notes.proto
│ ├── notetypes.proto
│ ├── scheduler.proto
│ ├── search.proto
│ ├── stats.proto
│ ├── sync.proto
│ └── tags.proto
├── pylib/
│ ├── .gitignore
│ ├── README.md
│ ├── anki/
│ │ ├── _backend.py
│ │ ├── _legacy.py
│ │ ├── _rsbridge.pyi
│ │ ├── _vendor/
│ │ │ └── stringcase.py
│ │ ├── browser.py
│ │ ├── cards.py
│ │ ├── collection.py
│ │ ├── config.py
│ │ ├── consts.py
│ │ ├── db.py
│ │ ├── dbproxy.py
│ │ ├── decks.py
│ │ ├── errors.py
│ │ ├── exporting.py
│ │ ├── find.py
│ │ ├── foreign_data/
│ │ │ ├── __init__.py
│ │ │ └── mnemosyne.py
│ │ ├── hooks.py
│ │ ├── httpclient.py
│ │ ├── importing/
│ │ │ ├── __init__.py
│ │ │ ├── anki2.py
│ │ │ ├── apkg.py
│ │ │ ├── base.py
│ │ │ ├── csvfile.py
│ │ │ ├── mnemo.py
│ │ │ └── noteimp.py
│ │ ├── lang.py
│ │ ├── latex.py
│ │ ├── media.py
│ │ ├── models.py
│ │ ├── notes.py
│ │ ├── py.typed
│ │ ├── rsbackend.py
│ │ ├── scheduler/
│ │ │ ├── __init__.py
│ │ │ ├── base.py
│ │ │ ├── dummy.py
│ │ │ ├── legacy.py
│ │ │ └── v3.py
│ │ ├── sound.py
│ │ ├── stats.py
│ │ ├── statsbg.py
│ │ ├── stdmodels.py
│ │ ├── storage.py
│ │ ├── sync.py
│ │ ├── syncserver.py
│ │ ├── tags.py
│ │ ├── template.py
│ │ ├── types.py
│ │ └── utils.py
│ ├── hatch_build.py
│ ├── pyproject.toml
│ ├── rsbridge/
│ │ ├── .gitignore
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ └── lib.rs
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── shared.py
│ │ ├── support/
│ │ │ ├── anki12-broken.anki
│ │ │ ├── anki12-due.anki
│ │ │ ├── anki12.anki
│ │ │ ├── anki2-alpha.anki2
│ │ │ ├── diffmodels1.anki
│ │ │ ├── diffmodels2-1.apkg
│ │ │ ├── diffmodels2-2.apkg
│ │ │ ├── diffmodels2.anki
│ │ │ ├── diffmodeltemplates-1.apkg
│ │ │ ├── diffmodeltemplates-2.apkg
│ │ │ ├── invalid-ords.anki
│ │ │ ├── media.apkg
│ │ │ ├── supermemo1.xml
│ │ │ ├── suspended12.anki
│ │ │ ├── text-2fields.txt
│ │ │ ├── text-tags.txt
│ │ │ ├── text-update.txt
│ │ │ ├── update1.apkg
│ │ │ └── update2.apkg
│ │ ├── test_cards.py
│ │ ├── test_collection.py
│ │ ├── test_decks.py
│ │ ├── test_exporting.py
│ │ ├── test_find.py
│ │ ├── test_flags.py
│ │ ├── test_importing.py
│ │ ├── test_latex.py
│ │ ├── test_media.py
│ │ ├── test_models.py
│ │ ├── test_schedv3.py
│ │ ├── test_stats.py
│ │ ├── test_template.py
│ │ └── test_utils.py
│ └── tools/
│ ├── genbuildinfo.py
│ ├── genhooks.py
│ └── hookslib.py
├── pyproject.toml
├── python/
│ ├── mkempty.py
│ ├── sphinx/
│ │ ├── build.py
│ │ ├── conf.py
│ │ └── index.rst
│ └── version.py
├── qt/
│ ├── README.md
│ ├── aqt/
│ │ ├── __init__.py
│ │ ├── _macos_helper.py
│ │ ├── about.py
│ │ ├── addcards.py
│ │ ├── addons.py
│ │ ├── ankihub.py
│ │ ├── browser/
│ │ │ ├── __init__.py
│ │ │ ├── browser.py
│ │ │ ├── card_info.py
│ │ │ ├── find_and_replace.py
│ │ │ ├── find_duplicates.py
│ │ │ ├── layout.py
│ │ │ ├── previewer.py
│ │ │ ├── sidebar/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── item.py
│ │ │ │ ├── model.py
│ │ │ │ ├── searchbar.py
│ │ │ │ ├── toolbar.py
│ │ │ │ └── tree.py
│ │ │ └── table/
│ │ │ ├── __init__.py
│ │ │ ├── model.py
│ │ │ ├── state.py
│ │ │ └── table.py
│ │ ├── changenotetype.py
│ │ ├── clayout.py
│ │ ├── colors.py
│ │ ├── customstudy.py
│ │ ├── data/
│ │ │ └── web/
│ │ │ ├── css/
│ │ │ │ ├── addonconf.scss
│ │ │ │ ├── deckbrowser.scss
│ │ │ │ ├── overview.scss
│ │ │ │ ├── reviewer-bottom.scss
│ │ │ │ ├── toolbar-bottom.scss
│ │ │ │ ├── toolbar.scss
│ │ │ │ └── webview.scss
│ │ │ └── js/
│ │ │ ├── deckbrowser.ts
│ │ │ ├── pycmd.d.ts
│ │ │ ├── reviewer-bottom.ts
│ │ │ ├── toolbar.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── vendor/
│ │ │ │ └── plot.js
│ │ │ └── webview.ts
│ │ ├── dbcheck.py
│ │ ├── debug_console.py
│ │ ├── deckbrowser.py
│ │ ├── deckchooser.py
│ │ ├── deckconf.py
│ │ ├── deckdescription.py
│ │ ├── deckoptions.py
│ │ ├── editcurrent.py
│ │ ├── editor.py
│ │ ├── emptycards.py
│ │ ├── errors.py
│ │ ├── exporting.py
│ │ ├── fields.py
│ │ ├── filtered_deck.py
│ │ ├── flags.py
│ │ ├── forms/
│ │ │ ├── __init__.py
│ │ │ ├── about.py
│ │ │ ├── about.ui
│ │ │ ├── addcards.py
│ │ │ ├── addcards.ui
│ │ │ ├── addfield.py
│ │ │ ├── addfield.ui
│ │ │ ├── addmodel.py
│ │ │ ├── addmodel.ui
│ │ │ ├── addonconf.py
│ │ │ ├── addonconf.ui
│ │ │ ├── addons.py
│ │ │ ├── addons.ui
│ │ │ ├── browser.py
│ │ │ ├── browser.ui
│ │ │ ├── browserdisp.py
│ │ │ ├── browserdisp.ui
│ │ │ ├── browseropts.py
│ │ │ ├── browseropts.ui
│ │ │ ├── changemap.py
│ │ │ ├── changemap.ui
│ │ │ ├── changemodel.py
│ │ │ ├── changemodel.ui
│ │ │ ├── clayout_top.py
│ │ │ ├── clayout_top.ui
│ │ │ ├── customstudy.py
│ │ │ ├── customstudy.ui
│ │ │ ├── dconf.py
│ │ │ ├── dconf.ui
│ │ │ ├── debug.py
│ │ │ ├── debug.ui
│ │ │ ├── editcurrent.py
│ │ │ ├── editcurrent.ui
│ │ │ ├── edithtml.py
│ │ │ ├── edithtml.ui
│ │ │ ├── emptycards.py
│ │ │ ├── emptycards.ui
│ │ │ ├── exporting.py
│ │ │ ├── exporting.ui
│ │ │ ├── fields.py
│ │ │ ├── fields.ui
│ │ │ ├── filtered_deck.py
│ │ │ ├── filtered_deck.ui
│ │ │ ├── finddupes.py
│ │ │ ├── finddupes.ui
│ │ │ ├── findreplace.py
│ │ │ ├── findreplace.ui
│ │ │ ├── forget.py
│ │ │ ├── forget.ui
│ │ │ ├── getaddons.py
│ │ │ ├── getaddons.ui
│ │ │ ├── importing.py
│ │ │ ├── importing.ui
│ │ │ ├── main.py
│ │ │ ├── main.ui
│ │ │ ├── modelopts.py
│ │ │ ├── modelopts.ui
│ │ │ ├── models.py
│ │ │ ├── models.ui
│ │ │ ├── preferences.py
│ │ │ ├── preferences.ui
│ │ │ ├── preview.py
│ │ │ ├── preview.ui
│ │ │ ├── profiles.py
│ │ │ ├── profiles.ui
│ │ │ ├── progress.py
│ │ │ ├── progress.ui
│ │ │ ├── reposition.py
│ │ │ ├── reposition.ui
│ │ │ ├── setgroup.py
│ │ │ ├── setgroup.ui
│ │ │ ├── setlang.py
│ │ │ ├── setlang.ui
│ │ │ ├── stats.py
│ │ │ ├── stats.ui
│ │ │ ├── studydeck.py
│ │ │ ├── studydeck.ui
│ │ │ ├── synclog.py
│ │ │ ├── synclog.ui
│ │ │ ├── taglimit.py
│ │ │ ├── taglimit.ui
│ │ │ ├── template.py
│ │ │ ├── template.ui
│ │ │ ├── widgets.py
│ │ │ └── widgets.ui
│ │ ├── gui_hooks.py
│ │ ├── import_export/
│ │ │ ├── __init__.py
│ │ │ ├── exporting.py
│ │ │ ├── import_dialog.py
│ │ │ └── importing.py
│ │ ├── importing.py
│ │ ├── legacy.py
│ │ ├── log.py
│ │ ├── main.py
│ │ ├── mediacheck.py
│ │ ├── mediasrv.py
│ │ ├── mediasync.py
│ │ ├── modelchooser.py
│ │ ├── models.py
│ │ ├── mpv.py
│ │ ├── notetypechooser.py
│ │ ├── operations/
│ │ │ ├── __init__.py
│ │ │ ├── card.py
│ │ │ ├── collection.py
│ │ │ ├── deck.py
│ │ │ ├── note.py
│ │ │ ├── notetype.py
│ │ │ ├── scheduling.py
│ │ │ └── tag.py
│ │ ├── overview.py
│ │ ├── package.py
│ │ ├── preferences.py
│ │ ├── profiles.py
│ │ ├── progress.py
│ │ ├── props.py
│ │ ├── py.typed
│ │ ├── qt/
│ │ │ ├── __init__.py
│ │ │ └── qt6.py
│ │ ├── reviewer.py
│ │ ├── schema_change_tracker.py
│ │ ├── sound.py
│ │ ├── stats.py
│ │ ├── studydeck.py
│ │ ├── stylesheets.py
│ │ ├── switch.py
│ │ ├── sync.py
│ │ ├── tagedit.py
│ │ ├── taglimit.py
│ │ ├── taskman.py
│ │ ├── theme.py
│ │ ├── toolbar.py
│ │ ├── tts.py
│ │ ├── undo.py
│ │ ├── update.py
│ │ ├── url_schemes.py
│ │ ├── utils.py
│ │ ├── webview.py
│ │ ├── widgetgallery.py
│ │ └── winpaths.py
│ ├── hatch_build.py
│ ├── icons/
│ │ ├── README.md
│ │ └── sidebar.afdesign
│ ├── launcher/
│ │ ├── Cargo.toml
│ │ ├── addon/
│ │ │ ├── __init__.py
│ │ │ └── manifest.json
│ │ ├── build.rs
│ │ ├── lin/
│ │ │ ├── README.md
│ │ │ ├── anki
│ │ │ ├── anki.1
│ │ │ ├── anki.desktop
│ │ │ ├── anki.xml
│ │ │ ├── anki.xpm
│ │ │ ├── build.sh
│ │ │ ├── install.sh
│ │ │ └── uninstall.sh
│ │ ├── mac/
│ │ │ ├── Info.plist
│ │ │ ├── build.sh
│ │ │ ├── dmg/
│ │ │ │ ├── build.sh
│ │ │ │ ├── dmg_ds_store
│ │ │ │ ├── set-dmg-settings.app/
│ │ │ │ │ └── Contents/
│ │ │ │ │ ├── Info.plist
│ │ │ │ │ ├── MacOS/
│ │ │ │ │ │ └── applet
│ │ │ │ │ ├── PkgInfo
│ │ │ │ │ ├── Resources/
│ │ │ │ │ │ ├── Scripts/
│ │ │ │ │ │ │ └── main.scpt
│ │ │ │ │ │ ├── applet.icns
│ │ │ │ │ │ ├── applet.rsrc
│ │ │ │ │ │ └── description.rtfd/
│ │ │ │ │ │ └── TXT.rtf
│ │ │ │ │ └── _CodeSignature/
│ │ │ │ │ └── CodeResources
│ │ │ │ └── set-dmg-settings.scpt
│ │ │ ├── entitlements.python.xml
│ │ │ ├── icon/
│ │ │ │ ├── Assets.car
│ │ │ │ ├── Assets.xcassets/
│ │ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ │ └── Contents.json
│ │ │ │ │ └── Contents.json
│ │ │ │ └── build.sh
│ │ │ ├── notarize.sh
│ │ │ └── stub.c
│ │ ├── pyproject.toml
│ │ ├── src/
│ │ │ ├── bin/
│ │ │ │ ├── anki_console.rs
│ │ │ │ └── build_win.rs
│ │ │ ├── main.rs
│ │ │ └── platform/
│ │ │ ├── mac.rs
│ │ │ ├── mod.rs
│ │ │ ├── unix.rs
│ │ │ └── windows.rs
│ │ ├── versions.py
│ │ └── win/
│ │ ├── anki-manifest.rc
│ │ ├── anki.exe.manifest
│ │ ├── anki.template.nsi
│ │ ├── build.bat
│ │ └── fileassoc.nsh
│ ├── mac/
│ │ ├── README.md
│ │ ├── anki_mac_helper/
│ │ │ ├── __init__.py
│ │ │ └── py.typed
│ │ ├── ankihelper.xcodeproj/
│ │ │ ├── project.pbxproj
│ │ │ ├── project.xcworkspace/
│ │ │ │ ├── contents.xcworkspacedata
│ │ │ │ ├── xcshareddata/
│ │ │ │ │ └── IDEWorkspaceChecks.plist
│ │ │ │ └── xcuserdata/
│ │ │ │ └── dae.xcuserdatad/
│ │ │ │ ├── UserInterfaceState.xcuserstate
│ │ │ │ └── xcschemes/
│ │ │ │ └── xcschememanagement.plist
│ │ │ └── xcuserdata/
│ │ │ └── dae.xcuserdatad/
│ │ │ └── xcschemes/
│ │ │ ├── ankihelper.xcscheme
│ │ │ └── xcschememanagement.plist
│ │ ├── appnap.swift
│ │ ├── build.sh
│ │ ├── helper_build.py
│ │ ├── pyproject.toml
│ │ ├── record.swift
│ │ ├── theme.swift
│ │ └── update-launcher-env
│ ├── pyproject.toml
│ ├── release/
│ │ ├── .gitignore
│ │ └── build.sh
│ ├── runanki.py
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_addons.py
│ │ └── test_i18n.py
│ └── tools/
│ ├── build_qrc.py
│ ├── build_ui.py
│ ├── color_svg.py
│ ├── extract_sass_vars.py
│ ├── genhooks_gui.py
│ └── runanki.system.in
├── rslib/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── bench.sh
│ ├── benches/
│ │ └── benchmark.rs
│ ├── build.rs
│ ├── i18n/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── check.rs
│ │ ├── extract.rs
│ │ ├── gather.rs
│ │ ├── python.rs
│ │ ├── src/
│ │ │ ├── generated.rs
│ │ │ ├── generated_launcher.rs
│ │ │ └── lib.rs
│ │ ├── typescript.rs
│ │ └── write_strings.rs
│ ├── io/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── error.rs
│ │ └── lib.rs
│ ├── linkchecker/
│ │ ├── Cargo.toml
│ │ ├── src/
│ │ │ └── lib.rs
│ │ └── tests/
│ │ └── links.rs
│ ├── process/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── proto/
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ ├── python.rs
│ │ ├── rust.rs
│ │ ├── src/
│ │ │ ├── generic_helpers.rs
│ │ │ └── lib.rs
│ │ └── typescript.rs
│ ├── proto_gen/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── rust_interface.rs
│ ├── src/
│ │ ├── adding.rs
│ │ ├── ankidroid/
│ │ │ ├── db.rs
│ │ │ ├── error.rs
│ │ │ ├── mod.rs
│ │ │ └── service.rs
│ │ ├── ankihub/
│ │ │ ├── http_client/
│ │ │ │ └── mod.rs
│ │ │ ├── login.rs
│ │ │ └── mod.rs
│ │ ├── backend/
│ │ │ ├── adding.rs
│ │ │ ├── ankidroid.rs
│ │ │ ├── ankihub.rs
│ │ │ ├── ankiweb.rs
│ │ │ ├── card_rendering.rs
│ │ │ ├── collection.rs
│ │ │ ├── config.rs
│ │ │ ├── dbproxy.rs
│ │ │ ├── error.rs
│ │ │ ├── i18n.rs
│ │ │ ├── import_export.rs
│ │ │ ├── mod.rs
│ │ │ ├── ops.rs
│ │ │ └── sync.rs
│ │ ├── browser_table.rs
│ │ ├── card/
│ │ │ ├── mod.rs
│ │ │ ├── service.rs
│ │ │ └── undo.rs
│ │ ├── card_rendering/
│ │ │ ├── mod.rs
│ │ │ ├── parser.rs
│ │ │ ├── service.rs
│ │ │ ├── tts/
│ │ │ │ ├── mod.rs
│ │ │ │ ├── other.rs
│ │ │ │ └── windows.rs
│ │ │ └── writer.rs
│ │ ├── cloze.rs
│ │ ├── collection/
│ │ │ ├── backup.rs
│ │ │ ├── mod.rs
│ │ │ ├── service.rs
│ │ │ ├── timestamps.rs
│ │ │ ├── transact.rs
│ │ │ └── undo.rs
│ │ ├── config/
│ │ │ ├── bool.rs
│ │ │ ├── deck.rs
│ │ │ ├── mod.rs
│ │ │ ├── notetype.rs
│ │ │ ├── number.rs
│ │ │ ├── schema11.rs
│ │ │ ├── string.rs
│ │ │ └── undo.rs
│ │ ├── dbcheck.rs
│ │ ├── deckconfig/
│ │ │ ├── mod.rs
│ │ │ ├── schema11.rs
│ │ │ ├── service.rs
│ │ │ ├── undo.rs
│ │ │ └── update.rs
│ │ ├── decks/
│ │ │ ├── addupdate.rs
│ │ │ ├── counts.rs
│ │ │ ├── current.rs
│ │ │ ├── filtered.rs
│ │ │ ├── limits.rs
│ │ │ ├── mod.rs
│ │ │ ├── name.rs
│ │ │ ├── remove.rs
│ │ │ ├── reparent.rs
│ │ │ ├── schema11.rs
│ │ │ ├── service.rs
│ │ │ ├── stats.rs
│ │ │ ├── tree.rs
│ │ │ └── undo.rs
│ │ ├── error/
│ │ │ ├── db.rs
│ │ │ ├── filtered.rs
│ │ │ ├── invalid_input.rs
│ │ │ ├── mod.rs
│ │ │ ├── network.rs
│ │ │ ├── not_found.rs
│ │ │ ├── search.rs
│ │ │ └── windows.rs
│ │ ├── findreplace.rs
│ │ ├── i18n/
│ │ │ ├── mod.rs
│ │ │ └── service.rs
│ │ ├── image_occlusion/
│ │ │ ├── imagedata.rs
│ │ │ ├── imageocclusion.rs
│ │ │ ├── mod.rs
│ │ │ ├── notetype.css
│ │ │ ├── notetype.rs
│ │ │ └── service.rs
│ │ ├── import_export/
│ │ │ ├── gather.rs
│ │ │ ├── insert.rs
│ │ │ ├── mod.rs
│ │ │ ├── package/
│ │ │ │ ├── apkg/
│ │ │ │ │ ├── export.rs
│ │ │ │ │ ├── import/
│ │ │ │ │ │ ├── cards.rs
│ │ │ │ │ │ ├── decks.rs
│ │ │ │ │ │ ├── media.rs
│ │ │ │ │ │ ├── mod.rs
│ │ │ │ │ │ └── notes.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── tests.rs
│ │ │ │ ├── colpkg/
│ │ │ │ │ ├── export.rs
│ │ │ │ │ ├── import.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── tests.rs
│ │ │ │ ├── media.rs
│ │ │ │ ├── meta.rs
│ │ │ │ └── mod.rs
│ │ │ ├── service.rs
│ │ │ └── text/
│ │ │ ├── csv/
│ │ │ │ ├── export.rs
│ │ │ │ ├── import.rs
│ │ │ │ ├── metadata.rs
│ │ │ │ └── mod.rs
│ │ │ ├── import.rs
│ │ │ ├── json.rs
│ │ │ └── mod.rs
│ │ ├── latex.rs
│ │ ├── lib.rs
│ │ ├── links.rs
│ │ ├── log.rs
│ │ ├── markdown.rs
│ │ ├── media/
│ │ │ ├── check.rs
│ │ │ ├── files.rs
│ │ │ ├── mod.rs
│ │ │ └── service.rs
│ │ ├── notes/
│ │ │ ├── mod.rs
│ │ │ ├── service.rs
│ │ │ └── undo.rs
│ │ ├── notetype/
│ │ │ ├── cardgen.rs
│ │ │ ├── checks.rs
│ │ │ ├── cloze_styling.css
│ │ │ ├── emptycards.rs
│ │ │ ├── fields.rs
│ │ │ ├── header.tex
│ │ │ ├── merge.rs
│ │ │ ├── mod.rs
│ │ │ ├── notetypechange.rs
│ │ │ ├── render.rs
│ │ │ ├── restore.rs
│ │ │ ├── schema11.rs
│ │ │ ├── schemachange.rs
│ │ │ ├── service.rs
│ │ │ ├── stock.rs
│ │ │ ├── styling.css
│ │ │ ├── templates.rs
│ │ │ └── undo.rs
│ │ ├── ops.rs
│ │ ├── preferences.rs
│ │ ├── prelude.rs
│ │ ├── progress.rs
│ │ ├── revlog/
│ │ │ ├── mod.rs
│ │ │ └── undo.rs
│ │ ├── scheduler/
│ │ │ ├── answering/
│ │ │ │ ├── current.rs
│ │ │ │ ├── learning.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── preview.rs
│ │ │ │ ├── relearning.rs
│ │ │ │ ├── review.rs
│ │ │ │ └── revlog.rs
│ │ │ ├── bury_and_suspend.rs
│ │ │ ├── congrats.rs
│ │ │ ├── filtered/
│ │ │ │ ├── card.rs
│ │ │ │ ├── custom_study.rs
│ │ │ │ └── mod.rs
│ │ │ ├── fsrs/
│ │ │ │ ├── error.rs
│ │ │ │ ├── memory_state.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── params.rs
│ │ │ │ ├── rescheduler.rs
│ │ │ │ ├── retention.rs
│ │ │ │ ├── simulator.rs
│ │ │ │ └── try_collect.rs
│ │ │ ├── mod.rs
│ │ │ ├── new.rs
│ │ │ ├── queue/
│ │ │ │ ├── builder/
│ │ │ │ │ ├── burying.rs
│ │ │ │ │ ├── gathering.rs
│ │ │ │ │ ├── intersperser.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── sized_chain.rs
│ │ │ │ │ └── sorting.rs
│ │ │ │ ├── entry.rs
│ │ │ │ ├── learning.rs
│ │ │ │ ├── main.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── undo.rs
│ │ │ ├── reviews.rs
│ │ │ ├── service/
│ │ │ │ ├── answering.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── states/
│ │ │ │ ├── filtered.rs
│ │ │ │ ├── learning.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── new.rs
│ │ │ │ ├── normal.rs
│ │ │ │ ├── preview.rs
│ │ │ │ ├── relearning.rs
│ │ │ │ ├── rescheduling.rs
│ │ │ │ └── review.rs
│ │ │ ├── states/
│ │ │ │ ├── filtered.rs
│ │ │ │ ├── fuzz.rs
│ │ │ │ ├── interval_kind.rs
│ │ │ │ ├── learning.rs
│ │ │ │ ├── load_balancer.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── new.rs
│ │ │ │ ├── normal.rs
│ │ │ │ ├── preview_filter.rs
│ │ │ │ ├── relearning.rs
│ │ │ │ ├── rescheduling_filter.rs
│ │ │ │ ├── review.rs
│ │ │ │ └── steps.rs
│ │ │ ├── timespan.rs
│ │ │ ├── timing.rs
│ │ │ └── upgrade.rs
│ │ ├── search/
│ │ │ ├── builder.rs
│ │ │ ├── card_mod_order.sql
│ │ │ ├── deck_order.sql
│ │ │ ├── mod.rs
│ │ │ ├── note_cards_order.sql
│ │ │ ├── note_decks_order.sql
│ │ │ ├── note_due_order.sql
│ │ │ ├── note_ease_order.sql
│ │ │ ├── note_interval_order.sql
│ │ │ ├── note_lapses_order.sql
│ │ │ ├── note_original_position_order.sql
│ │ │ ├── note_reps_order.sql
│ │ │ ├── notetype_order.sql
│ │ │ ├── parser.rs
│ │ │ ├── service/
│ │ │ │ ├── browser_table.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── search_node.rs
│ │ │ ├── sqlwriter.rs
│ │ │ ├── template_order.sql
│ │ │ └── writer.rs
│ │ ├── serde.rs
│ │ ├── services.rs
│ │ ├── stats/
│ │ │ ├── card.rs
│ │ │ ├── graphs/
│ │ │ │ ├── added.rs
│ │ │ │ ├── buttons.rs
│ │ │ │ ├── card_counts.rs
│ │ │ │ ├── eases.rs
│ │ │ │ ├── future_due.rs
│ │ │ │ ├── hours.rs
│ │ │ │ ├── intervals.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── retention.rs
│ │ │ │ ├── retrievability.rs
│ │ │ │ ├── reviews.rs
│ │ │ │ └── today.rs
│ │ │ ├── mod.rs
│ │ │ ├── service.rs
│ │ │ └── today.rs
│ │ ├── storage/
│ │ │ ├── card/
│ │ │ │ ├── active_new_cards.sql
│ │ │ │ ├── add_card.sql
│ │ │ │ ├── add_card_if_unique.sql
│ │ │ │ ├── add_or_update.sql
│ │ │ │ ├── at_or_above_position.sql
│ │ │ │ ├── congrats.sql
│ │ │ │ ├── data.rs
│ │ │ │ ├── deck_due_counts.sql
│ │ │ │ ├── due_cards.sql
│ │ │ │ ├── filtered.rs
│ │ │ │ ├── fix_due_new.sql
│ │ │ │ ├── fix_due_other.sql
│ │ │ │ ├── fix_ivl.sql
│ │ │ │ ├── fix_low_ease.sql
│ │ │ │ ├── fix_mod.sql
│ │ │ │ ├── fix_odue.sql
│ │ │ │ ├── fix_ordinal.sql
│ │ │ │ ├── get_card.sql
│ │ │ │ ├── get_card_entry.sql
│ │ │ │ ├── get_ignored_before_count.sql
│ │ │ │ ├── intraday_due.sql
│ │ │ │ ├── mod.rs
│ │ │ │ ├── new_cards.sql
│ │ │ │ ├── search_cards_of_notes_into_table.sql
│ │ │ │ ├── search_cids_setup.sql
│ │ │ │ ├── search_cids_setup_ordered.sql
│ │ │ │ ├── siblings_for_bury.sql
│ │ │ │ └── update_card.sql
│ │ │ ├── collection_timestamps.rs
│ │ │ ├── config/
│ │ │ │ ├── add.sql
│ │ │ │ ├── get.sql
│ │ │ │ ├── get_entry.sql
│ │ │ │ └── mod.rs
│ │ │ ├── dbcheck/
│ │ │ │ ├── invalid_ids_count.sql
│ │ │ │ ├── invalid_ids_create.sql
│ │ │ │ ├── invalid_ids_drop.sql
│ │ │ │ ├── invalid_ids_update.sql
│ │ │ │ └── mod.rs
│ │ │ ├── deck/
│ │ │ │ ├── active_deck_ids_sorted.sql
│ │ │ │ ├── add_or_update_deck.sql
│ │ │ │ ├── all_decks_and_original_of_search_cards.sql
│ │ │ │ ├── all_decks_of_search_notes.sql
│ │ │ │ ├── alloc_id.sql
│ │ │ │ ├── cards_for_deck.sql
│ │ │ │ ├── due_counts.sql
│ │ │ │ ├── get_deck.sql
│ │ │ │ ├── missing-decks.sql
│ │ │ │ ├── mod.rs
│ │ │ │ ├── update_active.sql
│ │ │ │ └── update_deck.sql
│ │ │ ├── deckconfig/
│ │ │ │ ├── add.sql
│ │ │ │ ├── add_if_unique.sql
│ │ │ │ ├── add_or_update.sql
│ │ │ │ ├── get.sql
│ │ │ │ ├── mod.rs
│ │ │ │ └── update.sql
│ │ │ ├── graves/
│ │ │ │ ├── add.sql
│ │ │ │ ├── mod.rs
│ │ │ │ └── remove.sql
│ │ │ ├── mod.rs
│ │ │ ├── note/
│ │ │ │ ├── add.sql
│ │ │ │ ├── add_if_unique.sql
│ │ │ │ ├── add_or_update.sql
│ │ │ │ ├── get.sql
│ │ │ │ ├── get_tags.sql
│ │ │ │ ├── get_without_fields.sql
│ │ │ │ ├── is_orphaned.sql
│ │ │ │ ├── mod.rs
│ │ │ │ ├── notes_types_checksums_decks.sql
│ │ │ │ ├── search_nids_setup.sql
│ │ │ │ ├── update.sql
│ │ │ │ └── update_tags.sql
│ │ │ ├── notetype/
│ │ │ │ ├── add_notetype.sql
│ │ │ │ ├── add_or_update.sql
│ │ │ │ ├── existing_cards.sql
│ │ │ │ ├── field_names_for_notes.sql
│ │ │ │ ├── get_fields.sql
│ │ │ │ ├── get_notetype.sql
│ │ │ │ ├── get_notetype_names.sql
│ │ │ │ ├── get_templates.sql
│ │ │ │ ├── get_use_counts.sql
│ │ │ │ ├── highest_card_ord.sql
│ │ │ │ ├── mod.rs
│ │ │ │ ├── update_fields.sql
│ │ │ │ ├── update_notetype_config.sql
│ │ │ │ └── update_templates.sql
│ │ │ ├── revlog/
│ │ │ │ ├── add.sql
│ │ │ │ ├── fix_props.sql
│ │ │ │ ├── get.sql
│ │ │ │ ├── mod.rs
│ │ │ │ ├── studied_today.sql
│ │ │ │ ├── studied_today_by_deck.sql
│ │ │ │ ├── time_of_last_review.sql
│ │ │ │ └── v2_upgrade.sql
│ │ │ ├── schema11.sql
│ │ │ ├── sqlite.rs
│ │ │ ├── sync.rs
│ │ │ ├── sync_check.rs
│ │ │ ├── tag/
│ │ │ │ ├── add.sql
│ │ │ │ ├── alloc_id.sql
│ │ │ │ ├── get.sql
│ │ │ │ ├── mod.rs
│ │ │ │ └── update.sql
│ │ │ └── upgrades/
│ │ │ ├── mod.rs
│ │ │ ├── schema11_downgrade.sql
│ │ │ ├── schema14_upgrade.sql
│ │ │ ├── schema15_upgrade.sql
│ │ │ ├── schema17_upgrade.sql
│ │ │ ├── schema18_downgrade.sql
│ │ │ └── schema18_upgrade.sql
│ │ ├── sync/
│ │ │ ├── collection/
│ │ │ │ ├── changes.rs
│ │ │ │ ├── chunks.rs
│ │ │ │ ├── download.rs
│ │ │ │ ├── finish.rs
│ │ │ │ ├── graves.rs
│ │ │ │ ├── meta.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── normal.rs
│ │ │ │ ├── progress.rs
│ │ │ │ ├── protocol.rs
│ │ │ │ ├── sanity.rs
│ │ │ │ ├── start.rs
│ │ │ │ ├── status.rs
│ │ │ │ ├── tests.rs
│ │ │ │ └── upload.rs
│ │ │ ├── error.rs
│ │ │ ├── http_client/
│ │ │ │ ├── full_sync.rs
│ │ │ │ ├── io_monitor.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── protocol.rs
│ │ │ ├── http_server/
│ │ │ │ ├── handlers.rs
│ │ │ │ ├── logging.rs
│ │ │ │ ├── media_manager/
│ │ │ │ │ ├── download.rs
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── upload.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── routes.rs
│ │ │ │ └── user.rs
│ │ │ ├── login.rs
│ │ │ ├── media/
│ │ │ │ ├── begin.rs
│ │ │ │ ├── changes.rs
│ │ │ │ ├── database/
│ │ │ │ │ ├── client/
│ │ │ │ │ │ ├── changetracker.rs
│ │ │ │ │ │ ├── mod.rs
│ │ │ │ │ │ └── schema.sql
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ └── server/
│ │ │ │ │ ├── entry/
│ │ │ │ │ │ ├── changes.rs
│ │ │ │ │ │ ├── changes.sql
│ │ │ │ │ │ ├── download.rs
│ │ │ │ │ │ ├── get_entry.sql
│ │ │ │ │ │ ├── mod.rs
│ │ │ │ │ │ ├── set_entry.sql
│ │ │ │ │ │ └── upload.rs
│ │ │ │ │ ├── meta/
│ │ │ │ │ │ ├── get_meta.sql
│ │ │ │ │ │ ├── mod.rs
│ │ │ │ │ │ └── set_meta.sql
│ │ │ │ │ ├── mod.rs
│ │ │ │ │ ├── schema_v3.sql
│ │ │ │ │ └── schema_v4.sql
│ │ │ │ ├── download.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── progress.rs
│ │ │ │ ├── protocol.rs
│ │ │ │ ├── sanity.rs
│ │ │ │ ├── syncer.rs
│ │ │ │ ├── tests.rs
│ │ │ │ ├── upload.rs
│ │ │ │ └── zip.rs
│ │ │ ├── mod.rs
│ │ │ ├── request/
│ │ │ │ ├── header_and_stream.rs
│ │ │ │ ├── mod.rs
│ │ │ │ └── multipart.rs
│ │ │ ├── response.rs
│ │ │ └── version.rs
│ │ ├── tags/
│ │ │ ├── bulkadd.rs
│ │ │ ├── complete.rs
│ │ │ ├── findreplace.rs
│ │ │ ├── matcher.rs
│ │ │ ├── mod.rs
│ │ │ ├── notes.rs
│ │ │ ├── register.rs
│ │ │ ├── remove.rs
│ │ │ ├── rename.rs
│ │ │ ├── reparent.rs
│ │ │ ├── service.rs
│ │ │ ├── tree.rs
│ │ │ └── undo.rs
│ │ ├── template.rs
│ │ ├── template_filters.rs
│ │ ├── tests.rs
│ │ ├── text.rs
│ │ ├── timestamp.rs
│ │ ├── typeanswer.rs
│ │ ├── types.rs
│ │ ├── undo/
│ │ │ ├── changes.rs
│ │ │ └── mod.rs
│ │ └── version.rs
│ ├── sync/
│ │ ├── Cargo.toml
│ │ └── main.rs
│ └── tests/
│ └── support/
│ └── mediacheck.anki2
├── run
├── run.bat
├── rust-toolchain.toml
├── tools/
│ ├── build
│ ├── build-arm-lin
│ ├── build-x64-mac
│ ├── build.bat
│ ├── clean
│ ├── dmypy
│ ├── install-n2
│ ├── minilints/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ ├── ninja.bat
│ ├── profile
│ ├── publish
│ ├── rebuild-web
│ ├── reload_webviews.py
│ ├── run-qt6.6
│ ├── run-qt6.7
│ ├── run-qt6.8
│ ├── run.py
│ ├── runopt
│ ├── unused-rust-deps
│ ├── update-launcher-env
│ ├── update-launcher-env.bat
│ └── web-watch
├── ts/
│ ├── .gitignore
│ ├── README.md
│ ├── bundle_svelte.mjs
│ ├── bundle_ts.mjs
│ ├── editable/
│ │ ├── ContentEditable.svelte
│ │ ├── Mathjax.svelte
│ │ ├── change-timer.ts
│ │ ├── content-editable.ts
│ │ ├── cooldown-timer.ts
│ │ ├── decorated.ts
│ │ ├── editable-base.scss
│ │ ├── frame-element.ts
│ │ ├── frame-handle.ts
│ │ ├── index.ts
│ │ ├── mathjax-element.svelte.ts
│ │ └── mathjax.ts
│ ├── editor/
│ │ ├── BrowserEditor.svelte
│ │ ├── ClozeButtons.svelte
│ │ ├── CodeMirror.svelte
│ │ ├── CollapseBadge.svelte
│ │ ├── CollapseLabel.svelte
│ │ ├── DuplicateLink.svelte
│ │ ├── EditingArea.svelte
│ │ ├── EditorField.svelte
│ │ ├── FieldDescription.svelte
│ │ ├── FieldState.svelte
│ │ ├── Fields.svelte
│ │ ├── HandleBackground.svelte
│ │ ├── HandleControl.svelte
│ │ ├── HandleLabel.svelte
│ │ ├── LabelContainer.svelte
│ │ ├── LabelName.svelte
│ │ ├── NoteCreator.svelte
│ │ ├── NoteEditor.svelte
│ │ ├── Notification.svelte
│ │ ├── PlainTextBadge.svelte
│ │ ├── PreviewButton.svelte
│ │ ├── ReviewerEditor.svelte
│ │ ├── RichTextBadge.svelte
│ │ ├── StickyBadge.svelte
│ │ ├── base.ts
│ │ ├── code-mirror.ts
│ │ ├── decorated-elements.ts
│ │ ├── destroyable.ts
│ │ ├── editor-base.scss
│ │ ├── editor-toolbar/
│ │ │ ├── AddonButtons.svelte
│ │ │ ├── BlockButtons.svelte
│ │ │ ├── BoldButton.svelte
│ │ │ ├── ColorPicker.svelte
│ │ │ ├── CommandIconButton.svelte
│ │ │ ├── EditorToolbar.svelte
│ │ │ ├── HighlightColorButton.svelte
│ │ │ ├── ImageOcclusionButton.svelte
│ │ │ ├── InlineButtons.svelte
│ │ │ ├── ItalicButton.svelte
│ │ │ ├── LatexButton.svelte
│ │ │ ├── NotetypeButtons.svelte
│ │ │ ├── OptionsButton.svelte
│ │ │ ├── OptionsButtons.svelte
│ │ │ ├── RemoveFormatButton.svelte
│ │ │ ├── RichTextClozeButtons.svelte
│ │ │ ├── SubscriptButton.svelte
│ │ │ ├── SuperscriptButton.svelte
│ │ │ ├── TemplateButtons.svelte
│ │ │ ├── TextAttributeButton.svelte
│ │ │ ├── TextColorButton.svelte
│ │ │ ├── UnderlineButton.svelte
│ │ │ ├── WithColorHelper.svelte
│ │ │ └── index.ts
│ │ ├── helpers.ts
│ │ ├── image-overlay/
│ │ │ ├── FloatButtons.svelte
│ │ │ ├── ImageOverlay.svelte
│ │ │ ├── SizeSelect.svelte
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── legacy.scss
│ │ ├── mathjax-overlay/
│ │ │ ├── MathjaxButtons.svelte
│ │ │ ├── MathjaxEditor.svelte
│ │ │ ├── MathjaxOverlay.svelte
│ │ │ └── index.ts
│ │ ├── old-editor-adapter.ts
│ │ ├── plain-text-input/
│ │ │ ├── PlainTextInput.svelte
│ │ │ ├── index.ts
│ │ │ ├── remove-prohibited.ts
│ │ │ └── transform.ts
│ │ ├── rich-text-input/
│ │ │ ├── CustomStyles.svelte
│ │ │ ├── RichTextInput.svelte
│ │ │ ├── RichTextStyles.svelte
│ │ │ ├── StyleLink.svelte
│ │ │ ├── StyleTag.svelte
│ │ │ ├── index.ts
│ │ │ ├── normalizing-node-store.ts
│ │ │ ├── rich-text-resolve.ts
│ │ │ └── transform.ts
│ │ ├── surround.ts
│ │ └── types.ts
│ ├── html-filter/
│ │ ├── element.ts
│ │ ├── helpers.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── node.ts
│ │ └── styling.ts
│ ├── lib/
│ │ ├── components/
│ │ │ ├── Absolute.svelte
│ │ │ ├── BackendProgressIndicator.svelte
│ │ │ ├── Badge.svelte
│ │ │ ├── ButtonGroup.svelte
│ │ │ ├── ButtonGroupItem.svelte
│ │ │ ├── ButtonToolbar.svelte
│ │ │ ├── CheckBox.svelte
│ │ │ ├── Col.svelte
│ │ │ ├── Collapsible.svelte
│ │ │ ├── ConfigInput.svelte
│ │ │ ├── Container.svelte
│ │ │ ├── DropdownDivider.svelte
│ │ │ ├── DropdownItem.svelte
│ │ │ ├── DynamicallySlottable.svelte
│ │ │ ├── EnumSelector.svelte
│ │ │ ├── EnumSelectorRow.svelte
│ │ │ ├── ErrorPage.svelte
│ │ │ ├── FloatingArrow.svelte
│ │ │ ├── HelpModal.svelte
│ │ │ ├── HelpSection.svelte
│ │ │ ├── Icon.svelte
│ │ │ ├── IconButton.svelte
│ │ │ ├── IconConstrain.svelte
│ │ │ ├── Item.svelte
│ │ │ ├── Label.svelte
│ │ │ ├── LabelButton.svelte
│ │ │ ├── Popover.svelte
│ │ │ ├── Portal.svelte
│ │ │ ├── RenderChildren.svelte
│ │ │ ├── RevertButton.svelte
│ │ │ ├── Row.svelte
│ │ │ ├── ScrollArea.svelte
│ │ │ ├── Select.svelte
│ │ │ ├── SelectOption.svelte
│ │ │ ├── SettingTitle.svelte
│ │ │ ├── Shortcut.svelte
│ │ │ ├── Spacer.svelte
│ │ │ ├── SpinBox.svelte
│ │ │ ├── StickyContainer.svelte
│ │ │ ├── Switch.svelte
│ │ │ ├── SwitchRow.svelte
│ │ │ ├── TitledContainer.svelte
│ │ │ ├── VirtualTable.svelte
│ │ │ ├── WithContext.svelte
│ │ │ ├── WithFloating.svelte
│ │ │ ├── WithOverlay.svelte
│ │ │ ├── WithState.svelte
│ │ │ ├── WithTooltip.svelte
│ │ │ ├── context-keys.ts
│ │ │ ├── helpers.ts
│ │ │ ├── icons.ts
│ │ │ ├── resizable.ts
│ │ │ └── types.ts
│ │ ├── domlib/
│ │ │ ├── content-editable.ts
│ │ │ ├── find-above.ts
│ │ │ ├── index.ts
│ │ │ ├── location/
│ │ │ │ ├── document.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── location.ts
│ │ │ │ ├── node.ts
│ │ │ │ ├── range.ts
│ │ │ │ └── selection.ts
│ │ │ ├── move-nodes.ts
│ │ │ ├── place-caret.ts
│ │ │ └── surround/
│ │ │ ├── apply/
│ │ │ │ ├── format.ts
│ │ │ │ └── index.ts
│ │ │ ├── build/
│ │ │ │ ├── add-merge.ts
│ │ │ │ ├── build-tree.ts
│ │ │ │ ├── extend-merge.ts
│ │ │ │ ├── format.ts
│ │ │ │ └── index.ts
│ │ │ ├── flat-range.ts
│ │ │ ├── index.ts
│ │ │ ├── match-type.ts
│ │ │ ├── split-text.ts
│ │ │ ├── surround-format.ts
│ │ │ ├── surround.test.ts
│ │ │ ├── surround.ts
│ │ │ ├── test-utils.ts
│ │ │ ├── tree/
│ │ │ │ ├── block-node.ts
│ │ │ │ ├── element-node.ts
│ │ │ │ ├── formatting-node.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── tree-node.ts
│ │ │ └── unsurround.test.ts
│ │ ├── generated/
│ │ │ ├── README.md
│ │ │ ├── ftl-helpers.ts
│ │ │ └── post.ts
│ │ ├── sass/
│ │ │ ├── _button-mixins.scss
│ │ │ ├── _color-palette.scss
│ │ │ ├── _functions.scss
│ │ │ ├── _root-vars.scss
│ │ │ ├── _vars.scss
│ │ │ ├── base.scss
│ │ │ ├── bootstrap-dark.scss
│ │ │ ├── bootstrap-forms.scss
│ │ │ ├── bootstrap-tooltip.scss
│ │ │ ├── breakpoints.scss
│ │ │ ├── buttons.scss
│ │ │ ├── card-counts.scss
│ │ │ ├── core.scss
│ │ │ ├── elevation.scss
│ │ │ ├── night-mode.scss
│ │ │ ├── panes.scss
│ │ │ └── scrollbar.scss
│ │ ├── sveltelib/
│ │ │ ├── action-list.ts
│ │ │ ├── closing-click.ts
│ │ │ ├── closing-keyup.ts
│ │ │ ├── composition.ts
│ │ │ ├── context-property.ts
│ │ │ ├── dom-mirror.ts
│ │ │ ├── dynamic-slotting.ts
│ │ │ ├── dynamicComponent.ts
│ │ │ ├── event-predicate.d.ts
│ │ │ ├── event-store.ts
│ │ │ ├── export-runtime.ts
│ │ │ ├── handler-list.ts
│ │ │ ├── input-handler.ts
│ │ │ ├── lifecycle-hooks.ts
│ │ │ ├── modal-closing.ts
│ │ │ ├── node-store.ts
│ │ │ ├── position/
│ │ │ │ ├── auto-update.ts
│ │ │ │ ├── position-algorithm.d.ts
│ │ │ │ ├── position-floating.ts
│ │ │ │ └── position-overlay.ts
│ │ │ ├── preferences.ts
│ │ │ ├── resize-store.ts
│ │ │ ├── shortcut.ts
│ │ │ ├── store-subscribe.ts
│ │ │ ├── subscribe-updates.ts
│ │ │ ├── theme.ts
│ │ │ └── toggleable.ts
│ │ ├── tag-editor/
│ │ │ ├── AutocompleteItem.svelte
│ │ │ ├── Tag.svelte
│ │ │ ├── TagDeleteBadge.svelte
│ │ │ ├── TagEditMode.svelte
│ │ │ ├── TagEditor.svelte
│ │ │ ├── TagInput.svelte
│ │ │ ├── TagSpacer.svelte
│ │ │ ├── TagWithTooltip.svelte
│ │ │ ├── TagsRow.svelte
│ │ │ ├── WithAutocomplete.svelte
│ │ │ ├── index.ts
│ │ │ ├── tag-options-button/
│ │ │ │ ├── TagAddButton.svelte
│ │ │ │ ├── TagOptionsButton.svelte
│ │ │ │ ├── TagsSelectedButton.svelte
│ │ │ │ └── index.ts
│ │ │ └── tags.ts
│ │ └── tslib/
│ │ ├── bridgecommand.ts
│ │ ├── cards.ts
│ │ ├── children-access.ts
│ │ ├── context-keys.ts
│ │ ├── cross-browser.ts
│ │ ├── dom.ts
│ │ ├── events.ts
│ │ ├── functional.ts
│ │ ├── globals.ts
│ │ ├── help-page.ts
│ │ ├── helpers.ts
│ │ ├── i18n/
│ │ │ ├── index.ts
│ │ │ └── utils.ts
│ │ ├── image-import.d.ts
│ │ ├── keys.ts
│ │ ├── nightmode.ts
│ │ ├── node.ts
│ │ ├── parsing.ts
│ │ ├── platform.ts
│ │ ├── progress.ts
│ │ ├── promise.ts
│ │ ├── runtime-require.ts
│ │ ├── shadow-dom.d.ts
│ │ ├── shortcuts.ts
│ │ ├── styling.ts
│ │ ├── time.test.ts
│ │ ├── time.ts
│ │ ├── typing.ts
│ │ ├── ui.ts
│ │ └── wrap.ts
│ ├── licenses.json
│ ├── mathjax/
│ │ ├── index.ts
│ │ └── mathjax-types.d.ts
│ ├── page.html
│ ├── reviewer/
│ │ ├── answering.ts
│ │ ├── browser_selector.ts
│ │ ├── images.ts
│ │ ├── index.ts
│ │ ├── index_wrapper.ts
│ │ ├── lib.test.ts
│ │ ├── preload.ts
│ │ ├── reviewer.scss
│ │ ├── reviewer_extras.scss
│ │ └── reviewer_extras.ts
│ ├── routes/
│ │ ├── +error.svelte
│ │ ├── +layout.svelte
│ │ ├── +layout.ts
│ │ ├── base.scss
│ │ ├── card-info/
│ │ │ ├── CardInfo.svelte
│ │ │ ├── CardInfoPlaceholder.svelte
│ │ │ ├── CardStats.svelte
│ │ │ ├── ForgettingCurve.svelte
│ │ │ ├── Revlog.svelte
│ │ │ ├── [cardId]/
│ │ │ │ ├── +page.svelte
│ │ │ │ ├── +page.ts
│ │ │ │ └── [previousId]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ └── forgetting-curve.ts
│ │ ├── change-notetype/
│ │ │ ├── Alert.svelte
│ │ │ ├── ChangeNotetypePage.svelte
│ │ │ ├── Mapper.svelte
│ │ │ ├── MapperRow.svelte
│ │ │ ├── NotetypeSelector.svelte
│ │ │ ├── SaveButton.svelte
│ │ │ ├── StickyHeader.svelte
│ │ │ ├── [...notetypeIds]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── change-notetype-base.scss
│ │ │ ├── index.ts
│ │ │ ├── lib.test.ts
│ │ │ └── lib.ts
│ │ ├── congrats/
│ │ │ ├── +page.svelte
│ │ │ ├── +page.ts
│ │ │ ├── CongratsPage.svelte
│ │ │ ├── congrats-base.scss
│ │ │ ├── index.ts
│ │ │ └── lib.ts
│ │ ├── deck-options/
│ │ │ ├── Addons.svelte
│ │ │ ├── AdvancedOptions.svelte
│ │ │ ├── AudioOptions.svelte
│ │ │ ├── AutoAdvance.svelte
│ │ │ ├── BuryOptions.svelte
│ │ │ ├── CardStateCustomizer.svelte
│ │ │ ├── ConfigSelector.svelte
│ │ │ ├── DailyLimits.svelte
│ │ │ ├── DateInput.svelte
│ │ │ ├── DeckOptionsPage.svelte
│ │ │ ├── DisplayOrder.svelte
│ │ │ ├── EasyDays.svelte
│ │ │ ├── EasyDaysInput.svelte
│ │ │ ├── FsrsOptions.svelte
│ │ │ ├── FsrsOptionsOuter.svelte
│ │ │ ├── GlobalLabel.svelte
│ │ │ ├── HtmlAddon.svelte
│ │ │ ├── LapseOptions.svelte
│ │ │ ├── NewOptions.svelte
│ │ │ ├── ParamsInput.svelte
│ │ │ ├── ParamsInputRow.svelte
│ │ │ ├── ParamsSearchRow.svelte
│ │ │ ├── SaveButton.svelte
│ │ │ ├── SimulatorModal.svelte
│ │ │ ├── SpinBoxFloatRow.svelte
│ │ │ ├── SpinBoxRow.svelte
│ │ │ ├── StepsInput.svelte
│ │ │ ├── StepsInputRow.svelte
│ │ │ ├── TabbedValue.svelte
│ │ │ ├── TextInputModal.svelte
│ │ │ ├── TimerOptions.svelte
│ │ │ ├── Warning.svelte
│ │ │ ├── [deckId]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── choices.ts
│ │ │ ├── deck-options-base.scss
│ │ │ ├── index.ts
│ │ │ ├── lib.test.ts
│ │ │ ├── lib.ts
│ │ │ ├── steps.test.ts
│ │ │ └── steps.ts
│ │ ├── graphs/
│ │ │ ├── +page.svelte
│ │ │ ├── AddedGraph.svelte
│ │ │ ├── AxisTicks.svelte
│ │ │ ├── ButtonsGraph.svelte
│ │ │ ├── CalendarGraph.svelte
│ │ │ ├── CardCounts.svelte
│ │ │ ├── CumulativeOverlay.svelte
│ │ │ ├── DifficultyGraph.svelte
│ │ │ ├── EaseGraph.svelte
│ │ │ ├── FutureDue.svelte
│ │ │ ├── Graph.svelte
│ │ │ ├── GraphRangeRadios.svelte
│ │ │ ├── GraphsPage.svelte
│ │ │ ├── HistogramGraph.svelte
│ │ │ ├── HourGraph.svelte
│ │ │ ├── HoverColumns.svelte
│ │ │ ├── InputBox.svelte
│ │ │ ├── IntervalsGraph.svelte
│ │ │ ├── NoDataOverlay.svelte
│ │ │ ├── PercentageRange.svelte
│ │ │ ├── RangeBox.svelte
│ │ │ ├── RetrievabilityGraph.svelte
│ │ │ ├── ReviewsGraph.svelte
│ │ │ ├── StabilityGraph.svelte
│ │ │ ├── TableData.svelte
│ │ │ ├── TodayStats.svelte
│ │ │ ├── Tooltip.svelte
│ │ │ ├── TrueRetention.svelte
│ │ │ ├── TrueRetentionCombined.svelte
│ │ │ ├── TrueRetentionSingle.svelte
│ │ │ ├── WithGraphData.svelte
│ │ │ ├── _true-retention-base.scss
│ │ │ ├── added.ts
│ │ │ ├── buttons.ts
│ │ │ ├── calendar.ts
│ │ │ ├── card-counts.ts
│ │ │ ├── difficulty.ts
│ │ │ ├── ease.ts
│ │ │ ├── future-due.ts
│ │ │ ├── graph-helpers.ts
│ │ │ ├── graph-styles.ts
│ │ │ ├── graphs-base.scss
│ │ │ ├── histogram-graph.ts
│ │ │ ├── hours.ts
│ │ │ ├── index.ts
│ │ │ ├── intervals.ts
│ │ │ ├── percentageRange.ts
│ │ │ ├── retrievability.ts
│ │ │ ├── reviews.ts
│ │ │ ├── simulator.ts
│ │ │ ├── today.ts
│ │ │ ├── tooltip-utils.svelte.ts
│ │ │ └── true-retention.ts
│ │ ├── image-occlusion/
│ │ │ ├── ImageOcclusionPage.svelte
│ │ │ ├── ImageOcclusionPicker.svelte
│ │ │ ├── MaskEditor.svelte
│ │ │ ├── Notes.svelte
│ │ │ ├── StickyFooter.svelte
│ │ │ ├── Tags.svelte
│ │ │ ├── Toast.svelte
│ │ │ ├── Toolbar.svelte
│ │ │ ├── [...imagePathOrNoteId]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── add-or-update-note.svelte.ts
│ │ │ ├── canvas-scale.ts
│ │ │ ├── fabric.d.ts
│ │ │ ├── image-occlusion-base.scss
│ │ │ ├── index.ts
│ │ │ ├── lib.ts
│ │ │ ├── mask-editor.ts
│ │ │ ├── notes-toolbar/
│ │ │ │ ├── MoreTools.svelte
│ │ │ │ ├── NotesToolbar.svelte
│ │ │ │ ├── TextFormatting.svelte
│ │ │ │ ├── index.ts
│ │ │ │ └── lib.ts
│ │ │ ├── review.scss
│ │ │ ├── review.ts
│ │ │ ├── shapes/
│ │ │ │ ├── base.ts
│ │ │ │ ├── ellipse.ts
│ │ │ │ ├── from-cloze.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── lib.ts
│ │ │ │ ├── polygon.ts
│ │ │ │ ├── position.ts
│ │ │ │ ├── rectangle.ts
│ │ │ │ ├── text.ts
│ │ │ │ └── to-cloze.ts
│ │ │ ├── store.ts
│ │ │ ├── tools/
│ │ │ │ ├── add-from-cloze.ts
│ │ │ │ ├── api.ts
│ │ │ │ ├── from-shapes.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── lib.ts
│ │ │ │ ├── more-tools.ts
│ │ │ │ ├── shortcuts.ts
│ │ │ │ ├── tool-aligns.ts
│ │ │ │ ├── tool-buttons.ts
│ │ │ │ ├── tool-cursor.ts
│ │ │ │ ├── tool-ellipse.ts
│ │ │ │ ├── tool-fill.ts
│ │ │ │ ├── tool-polygon.ts
│ │ │ │ ├── tool-rect.ts
│ │ │ │ ├── tool-text.ts
│ │ │ │ ├── tool-undo-redo.ts
│ │ │ │ └── tool-zoom.ts
│ │ │ └── types.ts
│ │ ├── import-anki-package/
│ │ │ ├── Header.svelte
│ │ │ ├── ImportAnkiPackagePage.svelte
│ │ │ ├── [...path]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── choices.ts
│ │ │ ├── import-anki-package-base.scss
│ │ │ └── index.ts
│ │ ├── import-csv/
│ │ │ ├── FieldMapper.svelte
│ │ │ ├── FileOptions.svelte
│ │ │ ├── ImportCsvPage.svelte
│ │ │ ├── ImportOptions.svelte
│ │ │ ├── MapperRow.svelte
│ │ │ ├── Preview.svelte
│ │ │ ├── [...path]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── choices.ts
│ │ │ ├── import-csv-base.scss
│ │ │ ├── index.ts
│ │ │ └── lib.ts
│ │ ├── import-page/
│ │ │ ├── DetailsTable.svelte
│ │ │ ├── ImportLogPage.svelte
│ │ │ ├── ImportPage.svelte
│ │ │ ├── QueueSummary.svelte
│ │ │ ├── StickyHeader.svelte
│ │ │ ├── TableCell.svelte
│ │ │ ├── TableCellWithTooltip.svelte
│ │ │ ├── [...path]/
│ │ │ │ ├── +page.svelte
│ │ │ │ └── +page.ts
│ │ │ ├── import-page-base.scss
│ │ │ ├── index.ts
│ │ │ ├── lib.ts
│ │ │ └── types.ts
│ │ └── tmp/
│ │ └── _page.ts
│ ├── src/
│ │ ├── app.d.ts
│ │ ├── app.html
│ │ └── hooks.client.js
│ ├── svelte.config.js
│ ├── tools/
│ │ ├── markpure.ts
│ │ └── sql_format.ts
│ ├── transform_ts.mjs
│ ├── tsconfig.json
│ ├── tsconfig_legacy.json
│ └── vite.config.ts
├── yarn
└── yarn.bat
================================================
FILE CONTENTS
================================================
================================================
FILE: .buildkite/linux/docker/Dockerfile
================================================
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND="noninteractive"
RUN useradd -d /state -m -u 998 user
RUN apt-get update && apt install --yes gnupg ca-certificates && \
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 32A37959C2FA5C3C99EFBC32A79206696452D198 \
&& echo "deb https://apt.buildkite.com/buildkite-agent stable main" > /etc/apt/sources.list.d/buildkite-agent.list \
&& apt-get update \
&& apt-get install --yes --no-install-recommends \
autoconf \
bash \
buildkite-agent \
ca-certificates \
curl \
findutils \
g++ \
gcc \
git \
grep \
libdbus-1-3 \
libegl1 \
libfontconfig1 \
libgl1 \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-base1.0 \
libgstreamer1.0-0 \
libnss3 \
libpulse-mainloop-glib0 \
libpulse-mainloop-glib0 \
libssl-dev \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxi6 \
libxkbcommon-x11-0 \
libxkbcommon0 \
libxkbfile1 \
libxrandr2 \
libxrender1 \
libxtst6 \
make \
pkg-config \
portaudio19-dev \
python3-dev \
rsync \
unzip \
zstd \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /etc/buildkite-agent/hooks && chown -R user /etc/buildkite-agent
COPY buildkite.cfg /etc/buildkite-agent/buildkite-agent.cfg
COPY environment /etc/buildkite-agent/hooks/environment
RUN mkdir /state/rust && chown user /state/rust
USER user
ENV CARGO_HOME=/state/rust/cargo
ENV RUSTUP_HOME=/state/rust/rustup
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --no-modify-path --default-toolchain none
WORKDIR /code/buildkite
ENTRYPOINT ["/usr/bin/buildkite-agent", "start"]
================================================
FILE: .buildkite/linux/docker/build.sh
================================================
#!/bin/bash
# builds an 'anki-[amd|arm]' image for the current platform
#
# for a cross-compile on recent Docker:
# docker buildx create --use
# docker run --privileged --rm tonistiigi/binfmt --install amd64
# docker buildx build --platform linux/amd64 --tag anki-amd64 . --load
. common.inc
DOCKER_BUILDKIT=1 docker build --tag anki-${platform} .
================================================
FILE: .buildkite/linux/docker/buildkite.cfg
================================================
name="lin-ci"
tags="queue=lin-ci"
build-path="/state/build"
hooks-path="/etc/buildkite-agent/hooks"
no-plugins=true
no-local-hooks=true
no-git-submodules=true
================================================
FILE: .buildkite/linux/docker/common.inc
================================================
#!/bin/bash
set -e
if [[ "$(uname -m)" == "x86_64" ]]; then
platform="amd"
else
platform="arm"
fi
================================================
FILE: .buildkite/linux/docker/environment
================================================
#!/bin/bash
if [[ "${BUILDKITE_COMMAND}" != ".buildkite/linux/entrypoint" &&
"${BUILDKITE_COMMAND}" != ".buildkite/linux/release-entrypoint" ]]; then
echo "Command not allowed: ${BUILDKITE_COMMAND}"
exit 1
fi
================================================
FILE: .buildkite/linux/docker/run.sh
================================================
#!/bin/bash
# - use './run.sh' to run in the foreground
# - use './run.sh serve' to daemonize.
set -e
. common.inc
if [ "$1" = "serve" ]; then
extra_args="-d --restart always"
else
extra_args="-it"
fi
name=anki-${platform}
# Stop and remove the existing container if it exists.
# This doesn't delete the associated volume.
if docker container inspect $name > /dev/null 2>&1; then
docker stop $name || true
docker container rm $name
fi
docker run $extra_args \
--name $name \
-v ${name}-state:/state \
-e BUILDKITE_AGENT_TOKEN \
-e BUILDKITE_AGENT_TAGS \
$name
================================================
FILE: .buildkite/linux/entrypoint
================================================
#!/bin/bash
set -e
export PATH="$PATH:/state/rust/cargo/bin"
export BUILD_ROOT=/state/build
export ONLINE_TESTS=1
echo "--- Install n2"
./tools/install-n2
echo "+++ Building and testing"
ln -sf out/node_modules .
if [ "$CLEAR_RUST" = "1" ]; then
rm -rf $BUILD_ROOT/rust
fi
rm -f out/build.ninja
./ninja pylib qt check
echo "--- Ensure libs importable"
SKIP_RUN=1 ./run
echo "--- Check Rust libs"
cargo install cargo-deny@0.19.0
cargo deny check
echo "--- Cleanup"
rm -rf /tmp/* || true
================================================
FILE: .buildkite/linux/release-entrypoint
================================================
#!/bin/bash
set -e
export PATH="$PATH:/state/rust/cargo/bin"
export BUILD_ROOT=/state/build
export RELEASE=2
ln -sf out/node_modules .
echo "--- Install n2"
./tools/install-n2
echo "+++ Building"
if [ $(uname -m) = "aarch64" ]; then
export PYTHONPATH=/usr/lib/python3/dist-packages
./ninja wheels:anki
else
./ninja bundle
fi
================================================
FILE: .buildkite/mac/entrypoint
================================================
#!/bin/bash
set -e
STATE=$(pwd)/../state/anki-ci
mkdir -p $STATE
echo "+++ Building and testing"
ln -sf out/node_modules .
SKIP_RUNNER_BUILD=0 BUILD_ROOT=$STATE/build ./ninja pylib qt wheels check
================================================
FILE: .buildkite/windows/entrypoint.bat
================================================
set PATH=c:\cargo\bin;%PATH%
echo +++ Building and testing
if exist \buildkite\state\out (
move \buildkite\state\out .
)
if exist \buildkite\state\node_modules (
move \buildkite\state\node_modules .
)
call tools\ninja build pylib qt check || exit /b 1
echo --- Cleanup
move out \buildkite\state\
move node_modules \buildkite\state\
================================================
FILE: .cargo/config.toml
================================================
[env]
STRINGS_PY = { value = "out/pylib/anki/_fluent.py", relative = true }
STRINGS_TS = { value = "out/ts/lib/generated/ftl.ts", relative = true }
DESCRIPTORS_BIN = { value = "out/rslib/proto/descriptors.bin", relative = true }
# build script will append .exe if necessary
PROTOC = { value = "out/extracted/protoc/bin/protoc", relative = true }
PYO3_NO_PYTHON = "1"
MACOSX_DEPLOYMENT_TARGET = "11"
PYTHONDONTWRITEBYTECODE = "1" # prevent junk files on Windows
[term]
color = "always"
[target.'cfg(all(target_env = "msvc", target_os = "windows"))']
rustflags = ["-C", "target-feature=+crt-static"]
================================================
FILE: .config/nextest.toml
================================================
[store]
dir = "out/tests/nextest"
================================================
FILE: .cursor/rules/building.md
================================================
- To build and check the project, use ./check in the root folder (or check.bat on Windows)
- This will format files, then run lints and unit tests.
================================================
FILE: .cursor/rules/i18n.md
================================================
- We use the fluent system+code generation for translation.
- New strings should be added to rslib/core/. Ask for the appropriate file if you're not sure.
- Assuming a string addons-you-have-count has been added to addons.ftl, that string is accessible in our different languages as follows:
- Python: from aqt.utils import tr; msg = tr.addons_you_have_count(count=3)
- TypeScript: import * as tr from "@generated/ftl"; tr.addonsYouHaveCount({count: 3})
- Rust: collection.tr.addons_you_have_count(3)
- In Qt .ui files, strings that are marked as translatable will automatically use the registered ftl strings. So a QLabel with a title 'addons_you_have_count' that is marked as translatable will automatically use the translation defined in our addons.ftl file.
================================================
FILE: .deny.toml
================================================
# all-features = true
# features = []
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [
# burn depends on an unmaintained package 'paste'
"RUSTSEC-2024-0436",
# bincode is unmaintained (via burn). Alternatives: postcard, bitcode, rkyv, wincode
"RUSTSEC-2025-0141",
# rustls-pemfile is unmaintained. Alternative: use rustls-pki-types directly (PemObject trait)
"RUSTSEC-2025-0134",
# unic-* crates are unmaintained (used for Unicode category detection).
# Alternative: icu_properties
"RUSTSEC-2025-0081", # unic-char-property
"RUSTSEC-2025-0075", # unic-char-range (or use native Rust char ranges since 1.45.0)
"RUSTSEC-2025-0080", # unic-common
"RUSTSEC-2025-0094", # unic-ucd-category
"RUSTSEC-2025-0098", # unic-ucd-version
]
[licenses]
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"CDLA-Permissive-2.0",
"ISC",
"MPL-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"CC0-1.0",
"Unlicense",
"Zlib",
"Unicode-3.0",
]
confidence-threshold = 0.8
# eg { allow = ["Zlib"], name = "adler32", version = "*" },
exceptions = []
[[licenses.clarify]]
name = "ring"
version = "*"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[licenses.private]
ignore = true
[sources]
unknown-registry = "warn"
unknown-git = "warn"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
[sources.allow-org]
github = ["ankitects"]
[bans]
multiple-versions = "allow"
wildcards = "allow"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"
# eg { name = "ansi_term", version = "=0.11.0" },
allow = []
deny = []
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = []
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
# eg { name = "ansi_term", version = "=0.11.0", depth = 20 },
skip-tree = []
================================================
FILE: .dockerignore
================================================
node_modules/
target/
out/
================================================
FILE: .dprint.json
================================================
{
"typescript": {
"indentWidth": 4,
"useBraces": "always"
},
"json": {
"indentWidth": 4
},
"markdown": {},
"toml": {},
"includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json,md,toml,svelte,scss}"],
"excludes": [
".vscode",
"**/node_modules",
"out/**",
"**/*-lock.json",
"qt/aqt/data/web/js/vendor/*.js",
"ftl/qt-repo",
"ftl/core-repo",
"ftl/usage",
"licenses.json",
".dmypy.json",
"target",
".mypy_cache",
"extra",
"ts/.svelte-kit",
"ts/vite.config.ts.timestamp*"
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.91.6.wasm",
"https://plugins.dprint.dev/json-0.19.3.wasm",
"https://plugins.dprint.dev/markdown-0.17.6.wasm",
"https://plugins.dprint.dev/toml-0.6.2.wasm",
"https://plugins.dprint.dev/disrupted/css-0.2.3.wasm"
]
}
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
root: true,
extends: ["eslint:recommended", "plugin:compat/recommended", "plugin:svelte/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
extraFileExtensions: [".svelte"],
},
plugins: [
"import",
"@typescript-eslint",
"@typescript-eslint/eslint-plugin",
],
rules: {
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"import/newline-after-import": "warn",
"import/no-useless-path-segments": "warn",
"prefer-const": "warn",
"no-nested-ternary": "warn",
"curly": "error",
"@typescript-eslint/consistent-type-imports": "error",
},
overrides: [
{
files: "**/*.ts",
extends: [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
},
},
{
files: ["*.svelte"],
parser: "svelte-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
},
rules: {
"svelte/no-at-html-tags": "off",
"svelte/valid-compile": ["error", { "ignoreWarnings": true }],
"@typescript-eslint/no-explicit-any": "off",
},
},
],
env: { browser: true, es2020: true },
ignorePatterns: ["backend_proto.d.ts", "*.svelte.d.ts", "vendor", "extra/*", "vite.config.ts", "hooks.client.js"],
globals: {
globalThis: false,
NodeListOf: false,
$$Generic: "readonly",
},
};
================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
*.ftl -linguist-detectable
cargo/remote/* linguist-vendored
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Developer Tasks
about: For bug reports, suggestions and support, please see the options below.
title: ""
labels: ""
assignees: ""
---
- Have a question or feature suggestion?
- Problems building/running on your system?
- Not 100% sure you've found a bug?
If so, please post on https://forums.ankiweb.net/ instead. This issue tracker is
intended primarily to track development tasks, and it is easier to provide support
over on the forums. Please make sure you read the following pages before
you post there:
- https://faqs.ankiweb.net/when-problems-occur.html
- https://faqs.ankiweb.net/getting-help.html
If you post questions, suggestions, or vague bug reports here, please do not be
offended if we close your ticket without replying. If in doubt, please post on
https://forums.ankiweb.net/ instead.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Bug Reports
url: https://forums.ankiweb.net
about: This issue tracker is for developers. Please report any bugs you encounter over on the user forum, and they will be triaged there.
- name: Questions/Support
url: https://forums.ankiweb.net
about: If you have a question or need support, please post on the user forum.
- name: Feature Requests/Suggestions
url: https://forums.ankiweb.net/c/suggestions/17
about: Please post suggestions and feature requests on our user forum.
================================================
FILE: .github/actions/setup-anki/action.yml
================================================
name: Setup Anki Build Environment
description: Install system dependencies, Rust toolchain, uv, and n2
runs:
using: composite
steps:
- name: Install Linux system dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
autoconf \
bash \
curl \
findutils \
g++ \
gcc \
git \
grep \
libdbus-1-3 \
libegl1 \
libfontconfig1 \
libgl1 \
libgstreamer-gl1.0-0 \
libgstreamer-plugins-base1.0 \
libgstreamer1.0-0 \
libnss3 \
libpulse-mainloop-glib0 \
libssl-dev \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxi6 \
libxkbcommon-x11-0 \
libxkbcommon0 \
libxkbfile1 \
libxrandr2 \
libxrender1 \
libxtst6 \
make \
pkg-config \
portaudio19-dev \
python3-dev \
rsync \
unzip \
zstd
- name: Install rsync (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
/c/msys64/usr/bin/pacman.exe -Sy --noconfirm rsync
echo "C:\msys64\usr\bin" >> "$GITHUB_PATH"
# Reads toolchain version from rust-toolchain.toml automatically.
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy
cache: false # ci.yml manages its own cargo cache
rustflags: "" # don't inject -D warnings; the build system handles this
- name: Install uv
id: setup-uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-python: true
prune-cache: ${{ runner.os != 'Windows' }} # Windows file-locking breaks cache pruning
# Set UV_CACHE_DIR so both setup-uv's binary and the build system's
# downloaded uv share the same dependency cache for faster builds.
- name: Configure uv cache
shell: bash
run: |
CACHE_DIR="${{ steps.setup-uv.outputs.cache-dir }}"
if [ -n "$CACHE_DIR" ]; then
echo "UV_CACHE_DIR=$CACHE_DIR" >> "$GITHUB_ENV"
fi
# UV_BINARY tells the build system to use our uv instead of downloading
# its own (see build/ninja_gen/src/python.rs). Linux-only because on
# macOS ARM the launcher needs the downloaded archive to build a universal
# binary via lipo (see build/configure/src/launcher.rs).
# On macOS/Windows, the downloaded uv will still use the shared cache.
- name: Set UV_BINARY (Linux)
if: runner.os == 'Linux'
shell: bash
run: echo "UV_BINARY=${{ steps.setup-uv.outputs.uv-path }}" >> "$GITHUB_ENV"
# Ensure cargo-installed tools like n2 are discoverable.
- name: Add CARGO_HOME/bin to PATH
shell: bash
run: echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> "$GITHUB_PATH"
- name: Install n2
shell: bash
run: |
if ! command -v n2 &>/dev/null; then
tools/install-n2
fi
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened, labeled]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
minilints:
runs-on: ubuntu-24.04
steps:
# Check out PR head commit so minilints can verify the author is in CONTRIBUTORS.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Run minilints
run: cargo run -p minilints -- check /tmp/minilints.stamp
env:
CONTRIBUTORS_BYPASS_EMAILS: ${{ vars.CONTRIBUTORS_BYPASS_EMAILS }}
# Lightweight formatting checks (no build outputs needed).
# Uses individual tool installs instead of setup-anki to stay fast.
format:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install n2
run: tools/install-n2
env:
RUSTFLAGS: "--cap-lints warn"
- name: Install just
uses: extractions/setup-just@v3
- name: Run format checks
run: just fmt
# Linux runs on every PR and push to main.
check-linux:
runs-on: ubuntu-24.04
name: check (linux)
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-
- name: Restore build output cache
uses: actions/cache/restore@v4
with:
path: out
key: build-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-Linux-${{ github.event.pull_request.number || 'main' }}-
build-Linux-main-
build-Linux-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@v3
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Build, lint, and test
env:
ONLINE_TESTS: "1"
run: |
just build
just lint
just test
- name: Ensure libs importable
env:
SKIP_RUN: "1"
run: ./run
- name: Check Rust dependencies
uses: EmbarkStudios/cargo-deny-action@v2
# out/pyenv contains a venv with absolute Python paths that break
# across runs. out/build.ninja is regenerated by configure each time.
# Remove both before saving so the cache stays portable.
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@v4
with:
path: out
key: build-Linux-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# Runs on pushes to main or on PRs with the check:macos label.
check-macos:
if: >-
github.event_name == 'push'
|| contains(github.event.pull_request.labels.*.name, 'check:macos')
runs-on: macos-latest
name: check (macos)
steps:
- uses: actions/checkout@v4
- name: Restore cargo cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
~/.cargo/bin
~/.cargo/.crates.toml
~/.cargo/.crates2.json
key: cargo-macOS-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-macOS-
- name: Restore build output cache
uses: actions/cache/restore@v4
with:
path: out
key: build-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-macOS-${{ github.event.pull_request.number || 'main' }}-
build-macOS-main-
build-macOS-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@v3
- name: Symlink node_modules
run: ln -sf out/node_modules .
- name: Build, lint, and test
run: |
just build
just wheels
just lint
just test
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@v4
with:
path: out
key: build-macOS-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
# Runs on pushes to main or on PRs with the check:windows label.
check-windows:
if: >-
github.event_name == 'push'
|| contains(github.event.pull_request.labels.*.name, 'check:windows')
runs-on: windows-latest
name: check (windows)
# Colocate CARGO_HOME and TEMP on D: to keep all I/O on the same fast
# local disk.
env:
CARGO_HOME: D:\cargo-home
TEMP: D:\tmp
TMP: D:\tmp
steps:
- uses: actions/checkout@v4
- name: Prepare D:\ directories
shell: bash
run: mkdir -p /d/cargo-home /d/tmp
- name: Restore cargo cache
uses: actions/cache@v4
with:
path: |
D:\cargo-home\registry\index
D:\cargo-home\registry\cache
D:\cargo-home\git\db
D:\cargo-home\bin
D:\cargo-home\.crates.toml
D:\cargo-home\.crates2.json
key: cargo-Windows-${{ hashFiles('Cargo.lock') }}
restore-keys: cargo-Windows-
- name: Restore build output cache
uses: actions/cache/restore@v4
with:
path: out
key: build-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
restore-keys: |
build-Windows-${{ github.event.pull_request.number || 'main' }}-
build-Windows-main-
build-Windows-
- name: Setup build environment
uses: ./.github/actions/setup-anki
- name: Install just
uses: extractions/setup-just@v3
- name: Build, lint, and test
run: |
just build
just lint
just test
# Also remove node_modules on Windows — file-locking corrupts the cache.
- name: Clean non-cacheable state
if: always()
shell: bash
run: |
rm -rf out/pyenv out/node_modules
rm -f out/build.ninja
- name: Save build output cache
if: always()
uses: actions/cache/save@v4
with:
path: out
key: build-Windows-${{ github.event.pull_request.number || 'main' }}-${{ github.run_id }}
================================================
FILE: .gitignore
================================================
__pycache__
.mypy_cache
.DS_Store
anki.prof
target
/user.bazelrc
.dmypy.json
/.idea/
/.vscode
/.bazel
/windows.bazelrc
/out
node_modules
.n2_db
.ninja_log
.ninja_deps
/extra
yarn-error.log
ts/.svelte-kit
.yarn
.claude/settings.local.json
.claude/user.md
================================================
FILE: .gitmodules
================================================
[submodule "ftl/core-repo"]
path = ftl/core-repo
url = https://github.com/ankitects/anki-core-i18n.git
shallow = true
[submodule "ftl/qt-repo"]
path = ftl/qt-repo
url = https://github.com/ankitects/anki-desktop-ftl.git
shallow = true
================================================
FILE: .idea.dist/repo.iml
================================================
================================================
FILE: .mypy.ini
================================================
[mypy]
python_version = 3.9
pretty = False
strict_optional = False
show_error_codes = True
check_untyped_defs = True
disallow_untyped_decorators = True
warn_redundant_casts = True
warn_unused_configs = True
strict_equality = True
namespace_packages = True
explicit_package_bases = True
mypy_path =
pylib,
out/pylib,
qt,
out/qt,
ftl,
pylib/tools,
python
exclude = (pylib/anki/_vendor)
[mypy-anki.*]
disallow_untyped_defs = True
[mypy-anki.importing.*]
disallow_untyped_defs = False
[mypy-anki.exporting]
disallow_untyped_defs = False
[mypy-aqt]
strict_optional = True
[mypy-aqt.browser.*]
strict_optional = True
[mypy-aqt.data.*]
strict_optional = True
[mypy-aqt.forms.*]
strict_optional = True
[mypy-aqt.import_export.*]
strict_optional = True
[mypy-aqt.operations.*]
strict_optional = True
[mypy-aqt.editor]
strict_optional = True
[mypy-aqt.importing]
strict_optional = True
[mypy-aqt.preferences]
strict_optional = True
[mypy-aqt.overview]
strict_optional = True
[mypy-aqt.customstudy]
strict_optional = True
[mypy-aqt.taglimit]
strict_optional = True
[mypy-aqt.modelchooser]
strict_optional = True
[mypy-aqt.deckdescription]
strict_optional = True
[mypy-aqt.deckbrowser]
strict_optional = True
[mypy-aqt.studydeck]
strict_optional = True
[mypy-aqt.tts]
strict_optional = True
[mypy-aqt.mediasrv]
strict_optional = True
[mypy-aqt.changenotetype]
strict_optional = True
[mypy-aqt.clayout]
strict_optional = True
[mypy-aqt.fields]
strict_optional = True
[mypy-aqt.filtered_deck]
strict_optional = True
[mypy-aqt.editcurrent]
strict_optional = True
[mypy-aqt.deckoptions]
strict_optional = True
[mypy-aqt.notetypechooser]
strict_optional = True
[mypy-aqt.stats]
strict_optional = True
[mypy-aqt.switch]
strict_optional = True
[mypy-aqt.debug_console]
strict_optional = True
[mypy-aqt.emptycards]
strict_optional = True
[mypy-aqt.flags]
strict_optional = True
[mypy-aqt.mediacheck]
strict_optional = True
[mypy-aqt.theme]
strict_optional = True
[mypy-aqt.toolbar]
strict_optional = True
[mypy-aqt.deckchooser]
strict_optional = True
[mypy-aqt.about]
strict_optional = True
[mypy-aqt.webview]
strict_optional = True
[mypy-aqt.mediasync]
strict_optional = True
[mypy-aqt.package]
strict_optional = True
[mypy-aqt.progress]
strict_optional = True
[mypy-aqt.tagedit]
strict_optional = True
[mypy-aqt.utils]
strict_optional = True
[mypy-aqt.sync]
strict_optional = True
[mypy-anki.scheduler.base]
strict_optional = True
[mypy-anki._backend.rsbridge]
ignore_missing_imports = True
[mypy-anki._vendor.stringcase]
disallow_untyped_defs = False
[mypy-stringcase]
ignore_missing_imports = True
[mypy-aqt.mpv]
disallow_untyped_defs = False
ignore_errors = True
[mypy-aqt.winpaths]
disallow_untyped_defs = False
[mypy-win32file]
ignore_missing_imports = True
[mypy-win32pipe]
ignore_missing_imports = True
[mypy-pywintypes]
ignore_missing_imports = True
[mypy-winerror]
ignore_missing_imports = True
[mypy-distro]
ignore_missing_imports = True
[mypy-win32api]
ignore_missing_imports = True
[mypy-xml.dom]
ignore_missing_imports = True
[mypy-psutil]
ignore_missing_imports = True
[mypy-bs4]
ignore_missing_imports = True
[mypy-fluent.*]
ignore_missing_imports = True
[mypy-compare_locales.*]
ignore_missing_imports = True
[mypy-PyQt5.*]
ignore_errors = True
ignore_missing_imports = True
[mypy-send2trash]
ignore_missing_imports = True
[mypy-win32com.*]
ignore_missing_imports = True
[mypy-jsonschema.*]
ignore_missing_imports = True
[mypy-socks]
ignore_missing_imports = True
[mypy-pythoncom]
ignore_missing_imports = True
[mypy-snakeviz.*]
ignore_missing_imports = True
[mypy-wheel.*]
ignore_missing_imports = True
[mypy-pip_system_certs.*]
ignore_missing_imports = True
[mypy-anki_audio]
ignore_missing_imports = True
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "all",
"printWidth": 88,
"tabWidth": 4,
"semi": true,
"htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-svelte"]
}
================================================
FILE: .python-version
================================================
3.13.5
================================================
FILE: .ruff.toml
================================================
lint.select = [
"E", # pycodestyle errors
"F", # Pyflakes errors
"PL", # Pylint rules
"I", # Isort rules
"ARG",
# "UP", # pyupgrade
# "B", # flake8-bugbear
# "SIM", # flake8-simplify
]
extend-exclude = ["*_pb2.py", "*_pb2.pyi"]
lint.ignore = [
# Docstring rules (missing-*-docstring in pylint)
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D103", # Missing docstring in public function
# Import rules (wrong-import-* in pylint)
"E402", # Module level import not at top of file
"E501", # Line too long
# pycodestyle rules
"E741", # ambiguous-variable-name
# Comment rules (fixme in pylint)
"FIX002", # Line contains TODO
# Pyflakes rules
"F402", # import-shadowed-by-loop-var
"F403", # undefined-local-with-import-star
"F405", # undefined-local-with-import-star-usage
# Naming rules (invalid-name in pylint)
"N801", # Class name should use CapWords convention
"N802", # Function name should be lowercase
"N803", # Argument name should be lowercase
"N806", # Variable in function should be lowercase
"N811", # Constant imported as non-constant
"N812", # Lowercase imported as non-lowercase
"N813", # Camelcase imported as lowercase
"N814", # Camelcase imported as constant
"N815", # Variable in class scope should not be mixedCase
"N816", # Variable in global scope should not be mixedCase
"N817", # CamelCase imported as acronym
"N818", # Error suffix in exception names
# Pylint rules
"PLW0603", # global-statement
"PLW2901", # redefined-loop-name
"PLC0415", # import-outside-top-level
"PLR2004", # magic-value-comparison
# Exception handling (broad-except, bare-except in pylint)
"BLE001", # Do not catch blind exception
# Argument rules (unused-argument in pylint)
"ARG001", # Unused function argument
"ARG002", # Unused method argument
"ARG005", # Unused lambda argument
# Access rules (protected-access in pylint)
"SLF001", # Private member accessed
# String formatting (consider-using-f-string in pylint)
"UP032", # Use f-string instead of format call
# Exception rules (broad-exception-raised in pylint)
"TRY301", # Abstract raise to an inner function
# Builtin shadowing (redefined-builtin in pylint)
"A001", # Variable shadows a Python builtin
"A002", # Argument shadows a Python builtin
"A003", # Class attribute shadows a Python builtin
]
[lint.per-file-ignores]
"**/anki/*_pb2.py" = ["ALL"]
[lint.pep8-naming]
ignore-names = ["id", "tr", "db", "ok", "ip"]
[lint.pylint]
max-args = 12
max-returns = 10
max-branches = 35
max-statements = 125
[lint.isort]
known-first-party = ["anki", "aqt", "tests"]
================================================
FILE: .rustfmt-empty.toml
================================================
================================================
FILE: .rustfmt.toml
================================================
# These settings are not supported on stable Rust, and are ignored by the ninja
# build script - to use them you need to run 'cargo +nightly fmt'
group_imports = "StdExternalCrate"
imports_granularity = "Item"
imports_layout = "Vertical"
wrap_comments = true
================================================
FILE: .version
================================================
25.09.2
================================================
FILE: .vscode.dist/extensions.json
================================================
{
"recommendations": [
"dprint.dprint",
"ms-python.python",
"charliermarsh.ruff",
"rust-lang.rust-analyzer",
"svelte.svelte-vscode",
"zxh404.vscode-proto3",
"usernamehw.errorlens",
"eamodio.gitlens"
]
}
================================================
FILE: .vscode.dist/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run",
"type": "debugpy",
"request": "launch",
"program": "tools/run.py",
"args": [
// "-p",
// "My test profile"
],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"python": "${workspaceFolder}/out/pyenv/bin/python",
"windows": {
"python": "${workspaceFolder}/out/pyenv/scripts/python.exe"
},
"env": {
"PYTHONWARNINGS": "default",
"PYTHONPYCACHEPREFIX": "out/pycache",
"ANKIDEV": "1",
"QTWEBENGINE_REMOTE_DEBUGGING": "8080",
"QTWEBENGINE_CHROMIUM_FLAGS": "--remote-allow-origins=http://localhost:8080",
"RUST_BACKTRACE": "1",
// "TRACESQL": "1",
// "HMR": "1",
"ANKI_API_PORT": "40000",
"ANKI_API_HOST": "127.0.0.1"
},
"justMyCode": true,
"preLaunchTask": "ninja"
}
]
}
================================================
FILE: .vscode.dist/settings.json
================================================
{
"editor.formatOnSave": true,
"[python]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/*/**": true,
".bazel/**": true
},
"python.analysis.extraPaths": [
"./pylib",
"out/pylib",
"./pylib/anki/_vendor",
"out/qt",
"qt"
],
"python.formatting.provider": "charliermarsh.ruff",
"python.linting.mypyEnabled": false,
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingModuleSource": "none"
},
"rust-analyzer.check.allTargets": false,
"rust-analyzer.files.excludeDirs": [".bazel", "node_modules"],
"rust-analyzer.procMacro.enable": true,
// this formats 'use' blocks in a nicer way, but requires you to run
// 'rustup install nightly'.
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"search.exclude": {
"**/node_modules": true,
".bazel/**": true
},
"rust-analyzer.cargo.buildScripts.enable": true,
"python.analysis.typeCheckingMode": "off",
"python.analysis.exclude": [
"out/launcher/**"
],
"terminal.integrated.env.windows": {
"PATH": "c:\\msys64\\usr\\bin;${env:Path}"
}
}
================================================
FILE: .vscode.dist/tasks.json
================================================
{
"version": "2.0.0",
"tasks": [
{
"label": "ninja",
"command": "ninja",
"args": [
"pylib",
"qt"
],
"windows": {
"command": "tools/ninja.bat",
"args": [
"pylib",
"qt"
]
}
}
]
}
================================================
FILE: .yarnrc.yml
================================================
nodeLinker: node-modules
enableScripts: false
================================================
FILE: CLAUDE.md
================================================
# Claude Code Configuration
## Project Overview
Anki is a spaced repetition flashcard program with a multi-layered architecture. Main components:
- Web frontend: Svelte/TypeScript in ts/
- PyQt GUI, which embeds the web components in aqt/
- Python library which wraps our rust Layer (pylib/, with Rust module in pylib/rsbridge)
- Core Rust layer in rslib/
- Protobuf definitions in proto/ that are used by the different layers to
talk to each other.
## Building/checking
./check (check.bat) will format the code and run the main build & checks.
Please do this as a final step before marking a task as completed.
## Quick iteration
During development, you can build/check subsections of our code:
- Rust: 'cargo check'
- Python: './tools/dmypy', and if wheel-related, './ninja wheels'
- TypeScript/Svelte: './ninja check:svelte'
Be mindful that some changes (such as modifications to .proto files) may
need a full build with './check' first.
## Build tooling
'./check' and './ninja' invoke our build system, which is implemented in build/. It takes care of downloading required deps and invoking our build
steps.
## Translations
ftl/ contains our Fluent translation files. We have scripts in rslib/i18n
to auto-generate an API for Rust, TypeScript and Python so that our code can
access the translations in a type-safe manner. Changes should be made to
ftl/core or ftl/qt. Except for features specific to our Qt interface, prefer
the core module. When adding new strings, confirm the appropriate ftl file
first, and try to match the existing style.
## Protobuf and IPC
Our build scripts use the .proto files to define our Rust library's
non-Rust API. pylib/rsbridge exposes that API, and _backend.py exposes
snake_case methods for each protobuf RPC that call into the API.
Similar tooling creates a @generated/backend TypeScript module for
communicating with the Rust backend (which happens over POST requests).
## Fixing errors
When dealing with build errors or failing tests, invoke 'check' or one
of the quick iteration commands regularly. This helps verify your changes
are correct. To locate other instances of a problem, run the check again -
don't attempt to grep the codebase.
## Ignores
The files in out/ are auto-generated. Mostly you should ignore that folder,
though you may sometimes find it useful to view out/{pylib/anki,qt/_aqt,ts/lib/generated} when dealing with cross-language communication or our other generated sourcecode.
## Launcher/installer
The code for our launcher is in qt/launcher, with separate code for each
platform.
## Rust dependencies
Prefer adding to the root workspace, and using dep.workspace = true in the individual Rust project.
## Rust utilities
rslib/{process,io} contain some helpers for file and process operations,
which provide better error messages/context and some ergonomics. Use them
when possible.
## Rust error handling
in rslib, use error/mod.rs's AnkiError/Result and snafu. In our other Rust modules, prefer anyhow + additional context where appropriate. Unwrapping
in build scripts/tests is fine.
## Individual preferences
See @.claude/user.md
================================================
FILE: CONTRIBUTORS
================================================
If you have made changes to Anki's AGPL code, you are welcome to distribute
the changed code under the AGPL license.
If you would like to contribute your code back to the official release, we ask
that you license your contributions under the BSD 3 clause license. Portions
of the code are also used in AnkiWeb and AnkiMobile, and accepting
contributions under an AGPL license would mean we could no longer use the code
we have written in those projects.
In your first pull request, please add your name below. By adding your name to
this file, you assert that any code you contribute to the Anki project is
licensed under the BSD 3 clause license. If any pull request you make contains
code that you don't own the copyright to, you agree to make that clear when
submitting the request.
When submitting a pull request, GitHub Actions will check that the Git email you
are submitting from matches the one you used to edit this file. A common issue
is adding yourself to this file using the username on your computer, but then
using GitHub to rebase or edit a pull request online. This will result in your
Git email becoming something like user@noreply.github.com. To prevent the
automatic check from failing, you can edit this file again using GitHub's online
editor, making a trivial edit like adding a space after your name, and then pull
requests will work regardless of whether you create them using your computer or
GitHub's online interface.
For users who previously confirmed the license of their contributions on the
support site, it would be great if you could add your name below as well.
********************
AMBOSS MD Inc.
Aristotelis P.
Erez Volk
zjosua
Yngve Hoiseth
Arthur Milchior
Ijgnd
Yoonchae Lee
Evandro Coan
Alan Du
Yuchen Lei
Henry Tang
Simone Gaiarin
Rai (Michal Pokorny)
Zeno Gantner
Henrik Giesel
Michał Bartoszkiewicz
Sander Santema
Thomas Brownback
Andrew Gaul
kenden
Emil Hamrin
Nickolay Yudin
neitrinoweb
Andreas Reis
Matt Krump
Alexander Presnyakov
Abdo
aplaice
phwoo
Soren Bjornstad
Aleksa Sarai
Jakub Kaczmarzyk
Akshara Balachandra
lukkea
David Allison
David Allison <62114487+david-allison@users.noreply.github.com>
Tsung-Han Yu
Piotr Kubowicz
RumovZ
Cecini
Krish Shah
ianki
rye761
Guillem Palau Salvà
Meredith Derecho
Daniel Wallgren
Kerrick Staley
Maksim Abramchuk
Benjamin Kulnik
Shaun Ren
Ryan Greenblatt
Matthias Metelka
qubist-pixel-ux
cherryblossom
Hikaru Yoshiga
Thore Tyborski
Alexander Giorev
Ren Tatsumoto
lolilolicon
Gesa Stupperich
git9527
Vova Selin
qxo <49526356@qq.com>
Spooghetti420
Danish Prakash
Araceli Yanez
Sam Bradshaw
gnnoh
Sachin Govind
Bruce Harris
Patric Cunha
Brayan Oliveira <69634269+BrayanDSO@users.noreply.github.com>
Luka Warren
wisherhxl
dobefore <1432338032@qq.com>
Bart Louwers
Sam Penny
Yutsuten
Zoom
TRIAEIOU
Stefan Kangas
Fabricio Duarte
Mani
Kaben Nanlohy
Tobias Predel
Daniel Tang
Jack Pearson
yellowjello
Ingemar Berg
Ben Kerman
Euan Kemp
Kieran Black
XeR
mgrottenthaler
Austin Siew
Joel Koen
Christopher Woggon
Kavel Rao
Ben Yip
mmjang <752515918@qq.com>
shunlog
3ter
Derek Dang
Luc Mcgrady
Kehinde Adeleke
Marko Juhanne
Gabriel Heinatz
Monty Evans
Nil Admirari
Michael Winkworth
Mateusz Wojewoda
Jarrett Ye
Sam Waechter
Michael Eliachevitch
Carlo Quick
Dominique Martinet
chandraiyengar
user1823 <92206575+user1823@users.noreply.github.com>
Gustaf Carefall
virinci
snowtimeglass
brishtibheja <136738526+brishtibheja@users.noreply.github.com>
Ben Olson
Akash Reddy
Lucio Sauer
Gustavo Sales
Shawn M Moore
Marko Sisovic
Viktor Ricci
Harvey Randall
Pedro Lameiras
Kai Knoblich
Lucas Scharenbroch
Antonio Cavallo
Han Yeong-woo
Jean Khawand
Pedro Schreiber
Foxy_null
Arbyste
Vasll
laalsaas
ijqq
AntoineQ1
jthulhu
Escape0707
Loudwig
Wu Yi-Wei
RRomeroJr <117.rromero@gmail.com>
Xidorn Quan
Alexander Bocken
James Elmore
Ian Samir Yep Manzano
David Culley <6276049+davidculley@users.noreply.github.com>
Rastislav Kish
jake
Expertium
Christian Donat
Asuka Minato
Dillon Baldwin
Voczi
Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com>
Themis Demetriades
Luke Bartholomew
Gregory Abrasaldo
Taylor Obyen <162023405+taylorobyen@users.noreply.github.com>
Kris Cherven
twwn
Cy Pokhrel
Park Hyunwoo
Tomas Fabrizio Orsi
Dongjin Ouyang <1113117424@qq.com>
Sawan Sunar
hideo aoyama
Ross Brown
🦙
Lukas Sommer
Luca Auer
Niclas Heinz
Omar Kohl
David Elizalde
beyondcompute
Yuki
wackbyte
GithubAnon0000
Mike Hardy
Danika_Dakika
Mumtaz Hajjo Alrifai
Thomas Graves
Jakub Fidler
Valerie Enfys
Julien Chol
ikkz
derivativeoflog7
rreemmii-dev
babofitos
Jonathan Schoreels
JL710
Matt Brubeck
Yaoliang Chen
KolbyML
Adnane Taghi
Spiritual Father
Emmanuel Ferdman
Sunong2008
Marvin Kopf
Kevin Nakamura
Bradley Szoke
jcznk
Thomas Rixen
Siyuan Mattuwu Yan
Lee Doughty <32392044+leedoughty@users.noreply.github.com>
memchr
Max Romanowski
Aldlss
Hanna Nilsén
Elias Johansson Lara
Toby Penner
Danilo Spillebeen
Matbe766
Amanda Sternberg
arold0
nav1s
Ranjit Odedra
Eltaurus
jariji
Francisco Esteva
Junia Mannervik
Emma Plante
SelfishPig
defkorean
Michael Lappas
Brett Schwartz
Lovro Boban
Yuuki Gabriele Patriarca
SecretX
Daniel Pechersky
fernandolins <1887601+fernandolins@users.noreply.github.com>
********************
The text of the 3 clause BSD license follows:
Contributions copyright the above contributors, 2010-Present.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: Cargo.toml
================================================
[workspace.package]
version = "0.0.0"
authors = ["Ankitects Pty Ltd and contributors "]
edition = "2021"
license = "AGPL-3.0-or-later"
rust-version = "1.80"
[workspace]
members = [
"build/configure",
"build/ninja_gen",
"build/runner",
"ftl",
"pylib/rsbridge",
"qt/launcher",
"rslib",
"rslib/i18n",
"rslib/io",
"rslib/linkchecker",
"rslib/process",
"rslib/proto",
"rslib/sync",
"tools/minilints",
]
resolver = "2"
[workspace.dependencies.percent-encoding-iri]
git = "https://github.com/ankitects/rust-url.git"
rev = "bb930b8d089f4d30d7d19c12e54e66191de47b88"
[workspace.dependencies.linkcheck]
git = "https://github.com/ankitects/linkcheck.git"
rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
[workspace.dependencies.fsrs]
version = "5.2.0"
# git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
# path = "../open-spaced-repetition/fsrs-rs"
[workspace.dependencies]
# local
anki = { path = "rslib" }
anki_i18n = { path = "rslib/i18n" }
anki_io = { path = "rslib/io" }
anki_process = { path = "rslib/process" }
anki_proto = { path = "rslib/proto" }
anki_proto_gen = { path = "rslib/proto_gen" }
ninja_gen = { "path" = "build/ninja_gen" }
# pinned
unicase = "=2.6.0" # any changes could invalidate sqlite indexes
# normal
ammonia = "4.1.2"
anyhow = "1.0.98"
async-compression = { version = "0.4.24", features = ["zstd", "tokio"] }
async-stream = "0.3.6"
async-trait = "0.1.88"
axum = { version = "0.8.4", features = ["multipart", "macros"] }
axum-client-ip = "1.1.3"
axum-extra = { version = "0.10.1", features = ["typed-header"] }
bitflags = "2.9.1"
blake3 = "1.8.2"
bytes = "1.11.1"
camino = "1.1.10"
chrono = { version = "0.4.41", default-features = false, features = ["std", "clock"] }
clap = { version = "4.5.40", features = ["derive"] }
coarsetime = "0.1.36"
convert_case = "0.8.0"
criterion = { version = "0.6.0" }
csv = "1.3.1"
data-encoding = "2.9.0"
difflib = "0.4.0"
dirs = "6.0.0"
dunce = "1.0.5"
embed-resource = "3.0.4"
envy = "0.4.2"
flate2 = "1.1.2"
fluent = "0.17.0"
fluent-bundle = "0.16.0"
fluent-syntax = "0.12.0"
fnv = "1.0.7"
futures = "0.3.31"
globset = "0.4.16"
hex = "0.4.3"
htmlescape = "0.3.1"
hyper = "1"
id_tree = "1.8.0"
inflections = "1.1.1"
intl-memoizer = "0.5.3"
itertools = "0.14.0"
junction = "1.2.0"
libc = "0.2"
libc-stdhandle = "0.1"
locale_config = "0.3.0"
maplit = "1.0.2"
nom = "8.0.0"
num-format = "0.4.4"
num_cpus = "1.17.0"
num_enum = "0.7.3"
once_cell = "1.21.3"
pbkdf2 = { version = "0.12", features = ["simple"] }
permutation = "0.4.1"
phf = { version = "0.11.3", features = ["macros"] }
pin-project = "1.1.10"
prettyplease = "0.2.34"
prost = "0.13"
prost-build = "0.13"
prost-reflect = "0.14.7"
prost-types = "0.13"
pulldown-cmark = "0.13.0"
pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] }
rand = "0.9.1"
rayon = "1.10.0"
regex = "1.11.1"
reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] }
rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] }
rustls-pemfile = "2.2.0"
scopeguard = "1.2.0"
serde = { version = "1.0.219", features = ["derive"] }
serde-aux = "4.7.0"
serde_json = "1.0.140"
serde_repr = "0.1.20"
serde_tuple = "1.1.0"
sha1 = "0.10.6"
sha2 = { version = "0.10.9" }
snafu = { version = "0.8.6", features = ["rust_1_61"] }
strum = { version = "0.27.1", features = ["derive"] }
syn = { version = "2.0.103", features = ["parsing", "printing"] }
tar = "0.4.44"
tempfile = "3.20.0"
termcolor = "1.4.1"
tokio = { version = "1.45", features = ["fs", "rt-multi-thread", "macros", "signal"] }
tokio-util = { version = "0.7.15", features = ["io"] }
tower-http = { version = "0.6.6", features = ["trace"] }
tracing = { version = "0.1.41", features = ["max_level_trace", "release_max_level_debug"] }
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] }
unic-langid = { version = "0.9.6", features = ["macros"] }
unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.24"
walkdir = "2.5.0"
which = "8.0.0"
widestring = "1.1.0"
winapi = { version = "0.3", features = ["wincon", "winreg"] }
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] }
wiremock = "0.6.3"
xz2 = "0.1.7"
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
zstd = { version = "0.13.3", features = ["zstdmt"] }
# Apply mild optimizations to our dependencies in dev mode, which among other things
# improves sha2 performance by about 21x. Opt 1 chosen due to
# https://doc.rust-lang.org/cargo/reference/profiles.html#overrides-and-generics. This
# applies to the dependencies of unit tests as well.
[profile.dev.package."*"]
opt-level = 1
debug = 0
[profile.dev.package.anki_i18n]
opt-level = 1
debug = 0
[profile.dev.package.anki_proto]
opt-level = 1
debug = 0
# Debug info off by default, which speeds up incremental builds and produces a considerably
# smaller library.
[profile.dev.package.anki]
debug = 0
[profile.dev.package.rsbridge]
debug = 0
[profile.release-lto]
inherits = "release"
lto = true
================================================
FILE: LICENSE
================================================
Anki is licensed under the GNU Affero General Public License, version 3 or
later, with portions contributed by Anki users licensed under the BSD-3
license (see CONTRIBUTORS).
The following included source code items use a license other than AGPL3:
In the pylib folder:
* statsbg.py: CC BY 4.0.
In the qt folder:
* Anki's translations are a mix of BSD and public domain.
* mpv.py: MIT.
* winpaths.py: MIT.
* MathJax: Apache 2.
* jQuery and jQuery-UI: MIT.
* plot.js: MIT.
* protobuf.js: BSD 3 clause
The above list only covers the source code that is vendored in this
repository. Binary distributions also include copies of Qt translation
files (LGPL), and all of the Python, Rust and Javascript libraries
that this code references.
Anki's logo is copyright Alex Fraser, and is licensed under the AGPL3 like the
rest of Anki's code.
The logo is also available under a limited alternative license for inclusion
in books, blogs, videos and so on. If the following conditions are met, you
may use the logo in your work without the need to license your work under an
AGPL3-compatible license:
* The logo must be used to refer to Anki, AnkiWeb, AnkiMobile or AnkiDroid,
and a link to https://apps.ankiweb.net must be provided. When your
content is focused specifically on AnkiDroid, a link to
https://play.google.com/store/apps/details?id=com.ichi2.anki&hl=en
may be provided instead of the first link.
* The work must make it clear that the text/video/etc you
are publishing is your own content and not something originating
from the Anki project.
* The logo must be used unmodified - no cropping, changing of colours
or adding or deleting content is allowed. You may resize the image
provided the horizontal and vertical dimensions are resized
equally.
================================================
FILE: README.md
================================================
# Anki®
[](https://buildkite.com/ankitects/anki-ci)
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
### 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://betas.ankiweb.net/)
### Developing
For more information on building and developing, please see [Development](./docs/development.md).
### Contributing
Want to contribute to Anki? Check out the [Contribution Guidelines](./docs/contributing.md).
### Anki Contributors
[CONTRIBUTORS](./CONTRIBUTORS)
# License
Anki's license: [LICENSE](./LICENSE)
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Reporting a Vulnerability
Anki does not currently have a bug bounty program, but if you have discovered a
security issue, a private message on our support site would be greatly
appreciated. No account is required to post a message:
https://anki.tenderapp.com/discussion/new
## FAQ
### Javascript on Cards/Templates
Anki allows users and shared deck authors to augment their card designs with
Javascript. This is used frequently, so disabling Javascript by default would
likely break a lot of the shared decks out there. That said, the default may be
changed in the future.
The computer version has a limited interface between Javascript and the parts of
Anki outside of the webview, so arbitrary code execution outside of the webview
should not be possible.
AnkiWeb hosts its study and editing interface on a separate ankiuser.net domain,
so that malicious Javascript on cards can not trigger endpoints hosted on the
main site. If you've found that not to be the case, or found an instance of JS
not being filtered on the main site, please let us know.
================================================
FILE: build/configure/Cargo.toml
================================================
[package]
name = "configure"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
publish = false
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
itertools.workspace = true
ninja_gen.workspace = true
================================================
FILE: build/configure/src/aqt.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::command::RunCommand;
use ninja_gen::copy::CopyFile;
use ninja_gen::copy::CopyFiles;
use ninja_gen::glob;
use ninja_gen::hashmap;
use ninja_gen::inputs;
use ninja_gen::node::CompileSass;
use ninja_gen::node::EsbuildScript;
use ninja_gen::node::TypescriptCheck;
use ninja_gen::python::python_format;
use ninja_gen::python::PythonTest;
use ninja_gen::rsync::RsyncFiles;
use ninja_gen::Build;
use ninja_gen::Utf8Path;
use ninja_gen::Utf8PathBuf;
use crate::anki_version;
use crate::python::BuildWheel;
use crate::web::copy_mathjax;
pub fn build_and_check_aqt(build: &mut Build) -> Result<()> {
build_forms(build)?;
build_generated_sources(build)?;
build_data_folder(build)?;
build_wheel(build)?;
check_python(build)?;
Ok(())
}
fn build_forms(build: &mut Build) -> Result<()> {
let ui_files = glob!["qt/aqt/forms/*.ui"];
let outdir = Utf8PathBuf::from("qt/_aqt/forms");
let mut py_files = vec![];
for path in ui_files.resolve() {
let outpath = outdir.join(path.file_name().unwrap()).into_string();
py_files.push(outpath.replace(".ui", "_qt6.py"));
}
build.add_action(
"qt:aqt:forms",
RunCommand {
command: ":pyenv:bin",
args: "$script $first_form",
inputs: hashmap! {
"script" => inputs!["qt/tools/build_ui.py"],
"" => inputs![ui_files],
},
outputs: hashmap! {
"first_form" => vec![py_files[0].as_str()],
"" => py_files.iter().skip(1).map(|s| s.as_str()).collect(),
},
},
)
}
/// For legacy reasons, we can not easily separate sources and generated files
/// up with a PEP420 namespace, as aqt/__init__.py exports a bunch of things.
/// To allow code to run/typecheck without having to merge source and generated
/// files into a separate folder, the generated files are exported as a separate
/// _aqt module.
fn build_generated_sources(build: &mut Build) -> Result<()> {
build.add_action(
"qt:aqt:hooks.py",
RunCommand {
command: ":pyenv:bin",
args: "$script $out",
inputs: hashmap! {
"script" => inputs!["qt/tools/genhooks_gui.py"],
"" => inputs!["pylib/anki/_vendor/stringcase.py", "pylib/tools/hookslib.py"]
},
outputs: hashmap! {
"out" => vec!["qt/_aqt/hooks.py"]
},
},
)?;
build.add_action(
"qt:aqt:sass_vars",
RunCommand {
command: ":pyenv:bin",
args: "$script $root_scss $out",
inputs: hashmap! {
"script" => inputs!["qt/tools/extract_sass_vars.py"],
"root_scss" => inputs![":css:_root-vars"],
},
outputs: hashmap! {
"out" => vec![
"qt/_aqt/colors.py",
"qt/_aqt/props.py"
]
},
},
)?;
// we need to add a py.typed file to the generated sources, or mypy
// will ignore them when used with the generated wheel
build.add_action(
"qt:aqt:py.typed",
CopyFile {
input: "qt/aqt/py.typed".into(),
output: "qt/_aqt/py.typed",
},
)?;
Ok(())
}
fn build_data_folder(build: &mut Build) -> Result<()> {
build_css(build)?;
build_imgs(build)?;
build_js(build)?;
build_pages(build)?;
build_icons(build)?;
copy_sveltekit(build)?;
Ok(())
}
fn copy_sveltekit(build: &mut Build) -> Result<()> {
build.add_action(
"qt:aqt:data:web:sveltekit",
RsyncFiles {
inputs: inputs![":sveltekit:folder"],
target_folder: "qt/_aqt/data/web/",
strip_prefix: "$builddir/",
extra_args: "-a --delete",
},
)
}
fn build_css(build: &mut Build) -> Result<()> {
let scss_files = build.expand_inputs(inputs![glob!["qt/aqt/data/web/css/*.scss"]]);
let out_dir = Utf8Path::new("qt/_aqt/data/web/css");
for scss in scss_files {
let stem = Utf8Path::new(&scss).file_stem().unwrap();
let mut out_path = out_dir.join(stem);
out_path.set_extension("css");
build.add_action(
"qt:aqt:data:web:css",
CompileSass {
input: scss.into(),
output: out_path.as_str(),
deps: inputs![":sass"],
load_paths: vec![".", "node_modules"],
},
)?;
}
let other_ts_css = build.inputs_with_suffix(
inputs![":ts:editor", ":ts:editable", ":ts:reviewer:reviewer.css"],
".css",
);
build.add_action(
"qt:aqt:data:web:css",
CopyFiles {
inputs: other_ts_css.into(),
output_folder: "qt/_aqt/data/web/css",
},
)
}
fn build_imgs(build: &mut Build) -> Result<()> {
build.add_action(
"qt:aqt:data:web:imgs",
CopyFiles {
inputs: inputs![glob!["qt/aqt/data/web/imgs/*"]],
output_folder: "qt/_aqt/data/web/imgs",
},
)
}
fn build_js(build: &mut Build) -> Result<()> {
for ts_file in &["deckbrowser", "webview", "toolbar", "reviewer-bottom"] {
build.add_action(
"qt:aqt:data:web:js",
EsbuildScript {
script: "ts/transform_ts.mjs".into(),
entrypoint: format!("qt/aqt/data/web/js/{ts_file}.ts").into(),
deps: inputs![],
output_stem: &format!("qt/_aqt/data/web/js/{ts_file}"),
extra_exts: &[],
},
)?;
}
let files = inputs![glob!["qt/aqt/data/web/js/*"]];
build.add_action(
"check:typescript:aqt",
TypescriptCheck {
tsconfig: "qt/aqt/data/web/js/tsconfig.json".into(),
inputs: files,
},
)?;
let files_from_ts = build.inputs_with_suffix(
inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"],
".js",
);
build.add_action(
"qt:aqt:data:web:js",
CopyFiles {
inputs: files_from_ts.into(),
output_folder: "qt/_aqt/data/web/js",
},
)?;
build_vendor_js(build)
}
fn build_vendor_js(build: &mut Build) -> Result<()> {
build.add_action("qt:aqt:data:web:js:vendor:mathjax", copy_mathjax())?;
build.add_action(
"qt:aqt:data:web:js:vendor",
CopyFiles {
inputs: inputs![
":node_modules:jquery",
":node_modules:jquery-ui",
":node_modules:bootstrap-dist",
"qt/aqt/data/web/js/vendor/plot.js"
],
output_folder: "qt/_aqt/data/web/js/vendor",
},
)
}
fn build_pages(build: &mut Build) -> Result<()> {
build.add_action(
"qt:aqt:data:web:pages",
CopyFiles {
inputs: inputs![":ts:pages"],
output_folder: "qt/_aqt/data/web/pages",
},
)?;
Ok(())
}
fn build_icons(build: &mut Build) -> Result<()> {
build_themed_icons(build)?;
build.add_action(
"qt:aqt:data:qt:icons:mdi_unthemed",
CopyFiles {
inputs: inputs![":node_modules:mdi_unthemed"],
output_folder: "qt/_aqt/data/qt/icons",
},
)?;
build.add_action(
"qt:aqt:data:qt:icons:from_src",
CopyFiles {
inputs: inputs![glob!["qt/aqt/data/qt/icons/*.{png,svg}"]],
output_folder: "qt/_aqt/data/qt/icons",
},
)?;
build.add_action(
"qt:aqt:data:qt:icons",
RunCommand {
command: ":pyenv:bin",
args: "$script $out $in",
inputs: hashmap! {
"script" => inputs!["qt/tools/build_qrc.py"],
"in" => inputs![
":qt:aqt:data:qt:icons:mdi_unthemed",
":qt:aqt:data:qt:icons:mdi_themed",
":qt:aqt:data:qt:icons:from_src",
]
},
outputs: hashmap! {
"out" => vec!["qt/_aqt/data/qt/icons.qrc"]
},
},
)?;
Ok(())
}
fn build_themed_icons(build: &mut Build) -> Result<()> {
let themed_icons_with_extra = hashmap! {
"chevron-up" => &["FG_DISABLED"],
"chevron-down" => &["FG_DISABLED"],
"drag-vertical" => &["FG_SUBTLE"],
"drag-horizontal" => &["FG_SUBTLE"],
"check" => &["FG_DISABLED"],
"circle-medium" => &["FG_DISABLED"],
"minus-thick" => &["FG_DISABLED"],
};
for icon_path in build.expand_inputs(inputs![":node_modules:mdi_themed"]) {
let path = Utf8Path::new(&icon_path);
let stem = path.file_stem().unwrap();
let mut colors = vec!["FG"];
if let Some(&extra) = themed_icons_with_extra.get(stem) {
colors.extend(extra);
}
build.add_action(
"qt:aqt:data:qt:icons:mdi_themed",
BuildThemedIcon {
src_icon: path,
colors,
},
)?;
}
Ok(())
}
struct BuildThemedIcon<'a> {
src_icon: &'a Utf8Path,
colors: Vec<&'a str>,
}
impl BuildAction for BuildThemedIcon<'_> {
fn command(&self) -> &str {
"$pyenv_bin $script $in $colors $out"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
let stem = self.src_icon.file_stem().unwrap();
// eg foo-light.svg, foo-dark.svg, foo-FG_SUBTLE-light.svg,
// foo-FG_SUBTLE-dark.svg
let outputs: Vec<_> = self
.colors
.iter()
.flat_map(|&color| {
let variant = if color == "FG" {
"".into()
} else {
format!("-{color}")
};
[
format!("qt/_aqt/data/qt/icons/{stem}{variant}-light.svg"),
format!("qt/_aqt/data/qt/icons/{stem}{variant}-dark.svg"),
]
})
.collect();
build.add_inputs("pyenv_bin", inputs![":pyenv:bin"]);
build.add_inputs("script", inputs!["qt/tools/color_svg.py"]);
build.add_inputs("in", inputs![self.src_icon.as_str()]);
build.add_inputs("", inputs![":qt:aqt:sass_vars"]);
build.add_variable("colors", self.colors.join(":"));
build.add_outputs("out", outputs);
}
}
fn build_wheel(build: &mut Build) -> Result<()> {
build.add_action(
"wheels:aqt",
BuildWheel {
name: "aqt",
version: anki_version(),
platform: None,
deps: inputs![
":qt:aqt",
glob!("qt/aqt/**"),
"qt/pyproject.toml",
"qt/hatch_build.py"
],
},
)
}
fn check_python(build: &mut Build) -> Result<()> {
python_format(build, "qt", inputs![glob!("qt/**/*.py")])?;
build.add_action(
"check:pytest:aqt",
PythonTest {
folder: "qt/tests",
python_path: &["pylib", "$builddir/pylib", "$builddir/qt"],
deps: inputs![":pylib:anki", ":qt:aqt", glob!["qt/tests/**"]],
},
)
}
================================================
FILE: build/configure/src/launcher.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::archives::download_and_extract;
use ninja_gen::archives::empty_manifest;
use ninja_gen::archives::OnlineArchive;
use ninja_gen::command::RunCommand;
use ninja_gen::hashmap;
use ninja_gen::inputs;
use ninja_gen::Build;
pub fn setup_uv_universal(build: &mut Build) -> Result<()> {
if !cfg!(target_arch = "aarch64") {
return Ok(());
}
build.add_action(
"launcher:uv_universal",
RunCommand {
command: "/usr/bin/lipo",
args: "-create -output $out $arm_bin $x86_bin",
inputs: hashmap! {
"arm_bin" => inputs![":extract:uv:bin"],
"x86_bin" => inputs![":extract:uv_mac_x86:bin"],
},
outputs: hashmap! {
"out" => vec!["launcher/uv"],
},
},
)
}
pub fn build_launcher(build: &mut Build) -> Result<()> {
setup_uv_universal(build)?;
download_and_extract(build, "nsis_plugins", NSIS_PLUGINS, empty_manifest())?;
Ok(())
}
const NSIS_PLUGINS: OnlineArchive = OnlineArchive {
url: "https://github.com/ankitects/anki-bundle-extras/releases/download/anki-2023-05-19/nsis.tar.zst",
sha256: "6133f730ece699de19714d0479c73bc848647d277e9cc80dda9b9ebe532b40a8",
};
================================================
FILE: build/configure/src/main.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod aqt;
mod launcher;
mod platform;
mod pylib;
mod python;
mod rust;
mod web;
use std::env;
use anyhow::Result;
use aqt::build_and_check_aqt;
use launcher::build_launcher;
use ninja_gen::glob;
use ninja_gen::inputs;
use ninja_gen::protobuf::check_proto;
use ninja_gen::protobuf::setup_protoc;
use ninja_gen::python::setup_uv;
use ninja_gen::Build;
use platform::overriden_python_venv_platform;
use pylib::build_pylib;
use pylib::check_pylib;
use python::check_python;
use python::setup_venv;
use rust::build_rust;
use rust::check_minilints;
use rust::check_rust;
use web::build_and_check_web;
use web::check_sql;
use crate::python::setup_sphinx;
fn anki_version() -> String {
std::fs::read_to_string(".version")
.unwrap()
.trim()
.to_string()
}
fn main() -> Result<()> {
let mut build = Build::new()?;
let build = &mut build;
setup_protoc(build)?;
check_proto(build, inputs![glob!["proto/**/*.proto"]])?;
if env::var("OFFLINE_BUILD").is_err() {
setup_uv(
build,
overriden_python_venv_platform().unwrap_or(build.host_platform),
)?;
}
setup_venv(build)?;
build_rust(build)?;
build_pylib(build)?;
build_and_check_web(build)?;
build_and_check_aqt(build)?;
if env::var("OFFLINE_BUILD").is_err() {
build_launcher(build)?;
}
setup_sphinx(build)?;
check_rust(build)?;
check_pylib(build)?;
check_python(build)?;
check_sql(build)?;
check_minilints(build)?;
build.trailing_text = "default pylib qt\n".into();
build.write_build_file()?;
Ok(())
}
================================================
FILE: build/configure/src/platform.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::env;
use ninja_gen::archives::Platform;
/// Please see [`overriden_python_target_platform()`] for details.
pub fn overriden_rust_target_triple() -> Option<&'static str> {
overriden_python_wheel_platform().map(|p| p.as_rust_triple())
}
/// Usually None to use the host architecture, except on Windows which
/// always uses x86_64, since WebEngine is unavailable for ARM64.
pub fn overriden_python_venv_platform() -> Option {
if cfg!(target_os = "windows") {
Some(Platform::WindowsX64)
} else {
None
}
}
/// Like [`overriden_python_venv_platform`], but:
/// If MAC_X86 is set, an X86 wheel will be built on macOS ARM.
/// If LIN_ARM64 is set, an ARM64 wheel will be built on Linux AMD64.
pub fn overriden_python_wheel_platform() -> Option {
if env::var("MAC_X86").is_ok() {
Some(Platform::MacX64)
} else if env::var("LIN_ARM64").is_ok() {
Some(Platform::LinuxArm)
} else {
overriden_python_venv_platform()
}
}
================================================
FILE: build/configure/src/pylib.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::archives::Platform;
use ninja_gen::command::RunCommand;
use ninja_gen::copy::LinkFile;
use ninja_gen::glob;
use ninja_gen::hashmap;
use ninja_gen::inputs;
use ninja_gen::python::python_format;
use ninja_gen::python::PythonTest;
use ninja_gen::Build;
use crate::anki_version;
use crate::platform::overriden_python_wheel_platform;
use crate::python::BuildWheel;
use crate::python::GenPythonProto;
pub fn build_pylib(build: &mut Build) -> Result<()> {
// generated files
build.add_action(
"pylib:anki:proto",
GenPythonProto {
proto_files: inputs![glob!["proto/anki/*.proto"]],
},
)?;
build.add_dependency("pylib:anki:proto", ":rslib:proto:py".into());
build.add_dependency("pylib:anki:i18n", ":rslib:i18n:py".into());
build.add_action(
"pylib:anki:hooks_gen.py",
RunCommand {
command: ":pyenv:bin",
args: "$script $out",
inputs: hashmap! {
"script" => inputs!["pylib/tools/genhooks.py"],
"" => inputs!["pylib/anki/_vendor/stringcase.py", "pylib/tools/hookslib.py"]
},
outputs: hashmap! {
"out" => vec!["pylib/anki/hooks_gen.py"]
},
},
)?;
build.add_action(
"pylib:anki:rsbridge",
LinkFile {
input: inputs![":pylib:rsbridge"],
output: &format!(
"pylib/anki/_rsbridge.{}",
match build.host_platform {
Platform::WindowsX64 | Platform::WindowsArm => "pyd",
_ => "so",
}
),
},
)?;
build.add_action("pylib:anki:buildinfo.py", GenBuildInfo {})?;
// wheel
build.add_action(
"wheels:anki",
BuildWheel {
name: "anki",
version: anki_version(),
platform: overriden_python_wheel_platform().or(Some(build.host_platform)),
deps: inputs![
":pylib:anki",
glob!("pylib/anki/**"),
"pylib/pyproject.toml",
"pylib/hatch_build.py"
],
},
)?;
Ok(())
}
pub fn check_pylib(build: &mut Build) -> Result<()> {
python_format(build, "pylib", inputs![glob!("pylib/**/*.py")])?;
build.add_action(
"check:pytest:pylib",
PythonTest {
folder: "pylib/tests",
python_path: &["$builddir/pylib"],
deps: inputs![":pylib:anki", glob!["pylib/{anki,tests}/**"]],
},
)
}
pub struct GenBuildInfo {}
impl BuildAction for GenBuildInfo {
fn command(&self) -> &str {
"$pyenv_bin $script $version_file $buildhash_file $out"
}
fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
build.add_inputs("pyenv_bin", inputs![":pyenv:bin"]);
build.add_inputs("script", inputs!["pylib/tools/genbuildinfo.py"]);
build.add_inputs("version_file", inputs![".version"]);
build.add_inputs("buildhash_file", inputs!["$builddir/buildhash"]);
build.add_outputs("out", vec!["pylib/anki/buildinfo.py"]);
}
}
================================================
FILE: build/configure/src/python.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::archives::Platform;
use ninja_gen::build::FilesHandle;
use ninja_gen::copy::CopyFiles;
use ninja_gen::glob;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs;
use ninja_gen::python::python_format;
use ninja_gen::python::PythonEnvironment;
use ninja_gen::python::PythonTypecheck;
use ninja_gen::python::RuffCheck;
use ninja_gen::Build;
/// Normalize version string by removing leading zeros from numeric parts
/// while preserving pre-release markers (b1, rc2, a3, etc.)
fn normalize_version(version: &str) -> String {
version
.split('.')
.map(|part| {
// Check if the part contains only digits
if part.chars().all(|c| c.is_ascii_digit()) {
// Numeric part: remove leading zeros
part.parse::().unwrap_or(0).to_string()
} else {
// Mixed part (contains both numbers and pre-release markers)
// Split on first non-digit character and normalize the numeric prefix
let chars = part.chars();
let mut numeric_prefix = String::new();
let mut rest = String::new();
let mut found_non_digit = false;
for ch in chars {
if ch.is_ascii_digit() && !found_non_digit {
numeric_prefix.push(ch);
} else {
found_non_digit = true;
rest.push(ch);
}
}
if numeric_prefix.is_empty() {
part.to_string()
} else {
let normalized_prefix = numeric_prefix.parse::().unwrap_or(0).to_string();
format!("{normalized_prefix}{rest}")
}
}
})
.collect::>()
.join(".")
}
pub fn setup_venv(build: &mut Build) -> Result<()> {
let extra_binary_exports = &["mypy", "ruff", "pytest", "protoc-gen-mypy"];
build.add_action(
"pyenv",
PythonEnvironment {
venv_folder: "pyenv",
deps: inputs![
"pyproject.toml",
"pylib/pyproject.toml",
"qt/pyproject.toml",
"uv.lock"
],
extra_args: "--all-packages --extra qt --extra audio",
extra_binary_exports,
},
)?;
Ok(())
}
pub struct GenPythonProto {
pub proto_files: BuildInput,
}
impl BuildAction for GenPythonProto {
fn command(&self) -> &str {
"$protoc $
--plugin=protoc-gen-mypy=$protoc-gen-mypy $
--python_out=$builddir/pylib $
--mypy_out=$builddir/pylib $
-Iproto $in"
}
fn files(&mut self, build: &mut impl FilesHandle) {
let proto_inputs = build.expand_inputs(&self.proto_files);
let python_outputs: Vec<_> = proto_inputs
.iter()
.flat_map(|path| {
let path = path
.replace('\\', "/")
.replace("proto/", "pylib/")
.replace(".proto", "_pb2");
[format!("{path}.py"), format!("{path}.pyi")]
})
.collect();
build.add_inputs("in", &self.proto_files);
build.add_inputs("protoc", inputs![":protoc_binary"]);
build.add_inputs("protoc-gen-mypy", inputs![":pyenv:protoc-gen-mypy"]);
build.add_outputs("", python_outputs);
}
fn hide_progress(&self) -> bool {
true
}
}
pub struct BuildWheel {
pub name: &'static str,
pub version: String,
pub platform: Option,
pub deps: BuildInput,
}
impl BuildAction for BuildWheel {
fn command(&self) -> &str {
"$uv build --wheel --out-dir=$out_dir --project=$project_dir"
}
fn files(&mut self, build: &mut impl FilesHandle) {
if std::env::var("OFFLINE_BUILD").ok().as_deref() == Some("1") {
let uv_path =
std::env::var("UV_BINARY").expect("UV_BINARY must be set in OFFLINE_BUILD mode");
build.add_inputs("uv", inputs![uv_path]);
} else {
build.add_inputs("uv", inputs![":uv_binary"]);
}
build.add_inputs("", &self.deps);
// Set the project directory based on which package we're building
let project_dir = if self.name == "anki" { "pylib" } else { "qt" };
build.add_variable("project_dir", project_dir);
// Set environment variable for uv to use our pyenv
build.add_variable("pyenv_path", "$builddir/pyenv");
build.add_env_var("UV_PROJECT_ENVIRONMENT", "$pyenv_path");
// Set output directory
build.add_variable("out_dir", "$builddir/wheels/");
// Calculate the wheel filename that uv will generate
let tag = if let Some(platform) = self.platform {
let platform_tag = match platform {
Platform::LinuxX64 => "manylinux_2_36_x86_64",
Platform::LinuxArm => "manylinux_2_36_aarch64",
Platform::MacX64 => "macosx_12_0_x86_64",
Platform::MacArm => "macosx_12_0_arm64",
Platform::WindowsX64 => "win_amd64",
Platform::WindowsArm => "win_arm64",
};
format!("cp39-abi3-{platform_tag}")
} else {
"py3-none-any".into()
};
// Set environment variable for hatch_build.py to use the correct platform tag
build.add_variable("wheel_tag", &tag);
build.add_env_var("ANKI_WHEEL_TAG", "$wheel_tag");
let name = self.name;
let normalized_version = normalize_version(&self.version);
let wheel_path = format!("wheels/{name}-{normalized_version}-{tag}.whl");
build.add_outputs("out", vec![wheel_path]);
}
}
pub fn check_python(build: &mut Build) -> Result<()> {
python_format(build, "tools", inputs![glob!("tools/**/*.py")])?;
build.add_action(
"check:mypy",
PythonTypecheck {
folders: &[
"pylib",
"qt/aqt",
"qt/tools",
"out/pylib/anki",
"out/qt/_aqt",
"python",
"tools",
],
deps: inputs![
glob!["{pylib,ftl,qt}/**/*.{py,pyi}"],
":pylib:anki",
":qt:aqt"
],
},
)?;
let ruff_folders = &["qt/aqt", "ftl", "pylib/tools", "tools", "python"];
let ruff_deps = inputs![
glob!["{pylib,ftl,qt,python,tools}/**/*.py"],
":pylib:anki",
":qt:aqt"
];
build.add_action(
"check:ruff",
RuffCheck {
folders: ruff_folders,
deps: ruff_deps.clone(),
check_only: true,
},
)?;
build.add_action(
"fix:ruff",
RuffCheck {
folders: ruff_folders,
deps: ruff_deps,
check_only: false,
},
)?;
Ok(())
}
struct Sphinx {
deps: BuildInput,
}
impl BuildAction for Sphinx {
fn command(&self) -> &str {
if std::env::var("OFFLINE_BUILD").ok().as_deref() == Some("1") {
"$python python/sphinx/build.py"
} else {
"$uv sync --extra sphinx && $python python/sphinx/build.py"
}
}
fn files(&mut self, build: &mut impl FilesHandle) {
if std::env::var("OFFLINE_BUILD").ok().as_deref() == Some("1") {
let uv_path =
std::env::var("UV_BINARY").expect("UV_BINARY must be set in OFFLINE_BUILD mode");
build.add_inputs("uv", inputs![uv_path]);
} else {
build.add_inputs("uv", inputs![":uv_binary"]);
// Set environment variable to use the existing pyenv
build.add_variable("pyenv_path", "$builddir/pyenv");
build.add_env_var("UV_PROJECT_ENVIRONMENT", "$pyenv_path");
}
build.add_inputs("python", inputs![":pyenv:bin"]);
build.add_inputs("", &self.deps);
build.add_output_stamp("python/sphinx/stamp");
}
fn hide_success(&self) -> bool {
false
}
}
pub(crate) fn setup_sphinx(build: &mut Build) -> Result<()> {
build.add_action(
"python:sphinx:copy_conf",
CopyFiles {
inputs: inputs![glob!("python/sphinx/{conf.py,index.rst}")],
output_folder: "python/sphinx",
},
)?;
build.add_action(
"python:sphinx",
Sphinx {
deps: inputs![
":pylib",
":qt",
":python:sphinx:copy_conf",
"pyproject.toml"
],
},
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_version_basic() {
assert_eq!(normalize_version("1.2.3"), "1.2.3");
assert_eq!(normalize_version("01.02.03"), "1.2.3");
assert_eq!(normalize_version("1.0.0"), "1.0.0");
}
#[test]
fn test_normalize_version_with_prerelease() {
assert_eq!(normalize_version("1.2.3b1"), "1.2.3b1");
assert_eq!(normalize_version("01.02.03b1"), "1.2.3b1");
assert_eq!(normalize_version("1.0.0rc2"), "1.0.0rc2");
assert_eq!(normalize_version("2.1.0a3"), "2.1.0a3");
assert_eq!(normalize_version("1.2.3beta1"), "1.2.3beta1");
assert_eq!(normalize_version("1.2.3alpha1"), "1.2.3alpha1");
}
}
================================================
FILE: build/configure/src/rust.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::env;
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::build::BuildProfile;
use ninja_gen::build::FilesHandle;
use ninja_gen::cargo::CargoBuild;
use ninja_gen::cargo::CargoClippy;
use ninja_gen::cargo::CargoFormat;
use ninja_gen::cargo::CargoTest;
use ninja_gen::cargo::RustOutput;
use ninja_gen::git::SyncSubmodule;
use ninja_gen::glob;
use ninja_gen::hash::simple_hash;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs;
use ninja_gen::Build;
use crate::platform::overriden_rust_target_triple;
pub fn build_rust(build: &mut Build) -> Result<()> {
prepare_translations(build)?;
build_proto_descriptors_and_interfaces(build)?;
build_rsbridge(build)
}
fn prepare_translations(build: &mut Build) -> Result<()> {
let offline_build = env::var("OFFLINE_BUILD").is_ok();
// ensure repos are checked out
build.add_action(
"ftl:repo:core",
SyncSubmodule {
path: "ftl/core-repo",
offline_build,
},
)?;
build.add_action(
"ftl:repo:qt",
SyncSubmodule {
path: "ftl/qt-repo",
offline_build,
},
)?;
// build anki_i18n and spit out strings.json
build.add_action(
"rslib:i18n",
CargoBuild {
inputs: inputs![
glob!["rslib/i18n/**"],
glob!["ftl/{core,core-repo,qt,qt-repo}/**"],
":ftl:repo",
],
outputs: &[
RustOutput::Data("py", "pylib/anki/_fluent.py"),
RustOutput::Data("ts", "ts/lib/generated/ftl.ts"),
],
target: None,
extra_args: "-p anki_i18n",
release_override: None,
},
)?;
build.add_action(
"ftl:bin",
CargoBuild {
inputs: inputs![glob!["ftl/**"],],
outputs: &[RustOutput::Binary("ftl")],
target: None,
extra_args: "-p ftl",
release_override: None,
},
)?;
// These don't use :group notation, as it doesn't make sense to invoke multiple
// commands as a group.
build.add_action(
"ftl-sync",
FtlCommand {
args: "sync",
deps: inputs![":ftl:repo", glob!["ftl/**"]],
},
)?;
build.add_action(
"ftl-deprecate",
FtlCommand {
args: "deprecate --ftl-roots ftl/core ftl/qt --source-roots pylib qt rslib ts --json-roots ftl/usage",
deps: inputs!["ftl/core", "ftl/qt", "pylib", "qt", "rslib", "ts"],
},
)?;
Ok(())
}
struct FtlCommand {
args: &'static str,
deps: BuildInput,
}
impl BuildAction for FtlCommand {
fn command(&self) -> &str {
"$ftl_bin $args"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("", &self.deps);
build.add_inputs("ftl_bin", inputs![":ftl:bin"]);
build.add_variable("args", self.args);
build.add_output_stamp(format!("ftl/stamp.{}", simple_hash(self.args)));
}
}
fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<()> {
let outputs = vec![
RustOutput::Data("descriptors.bin", "rslib/proto/descriptors.bin"),
RustOutput::Data("py", "pylib/anki/_backend_generated.py"),
RustOutput::Data("ts", "ts/lib/generated/backend.ts"),
];
build.add_action(
"rslib:proto",
CargoBuild {
inputs: inputs![glob!["{proto,rslib/proto}/**"], ":protoc_binary",],
outputs: &outputs,
target: None,
extra_args: "-p anki_proto",
release_override: None,
},
)?;
Ok(())
}
fn build_rsbridge(build: &mut Build) -> Result<()> {
let features = if cfg!(target_os = "linux") {
"rustls"
} else {
"native-tls"
};
build.add_action(
"pylib:rsbridge",
CargoBuild {
inputs: inputs![
glob!["{pylib/rsbridge/**,rslib/**}"],
// declare a dependency on i18n/proto so they get built first, allowing
// things depending on them to build faster, and ensuring
// changes to the ftl files trigger a rebuild
":rslib:i18n",
":rslib:proto",
// when env vars change the build hash gets updated
"$builddir/env",
"$builddir/buildhash",
// building on Windows requires python3.lib
if cfg!(windows) {
inputs![":pyenv:bin"]
} else {
inputs![]
}
],
outputs: &[RustOutput::DynamicLib("rsbridge")],
target: overriden_rust_target_triple(),
extra_args: &format!("-p rsbridge --features {features}"),
release_override: None,
},
)
}
pub fn check_rust(build: &mut Build) -> Result<()> {
let inputs = inputs![
glob!("{rslib/**,pylib/rsbridge/**,ftl/**,build/**,qt/launcher/**,tools/minilints/**}"),
"Cargo.lock",
"Cargo.toml",
"rust-toolchain.toml",
];
build.add_action(
"check:format:rust",
CargoFormat {
inputs: inputs.clone(),
check_only: true,
working_dir: Some("cargo/format"),
},
)?;
build.add_action(
"format:rust",
CargoFormat {
inputs: inputs.clone(),
check_only: false,
working_dir: Some("cargo/format"),
},
)?;
let inputs = inputs![
inputs,
// defer tests until build has completed; ensure re-run on changes
":pylib:rsbridge"
];
build.add_action(
"check:clippy",
CargoClippy {
inputs: inputs.clone(),
},
)?;
build.add_action("check:rust_test", CargoTest { inputs })?;
Ok(())
}
pub fn check_minilints(build: &mut Build) -> Result<()> {
struct RunMinilints {
pub deps: BuildInput,
pub fix: bool,
}
impl BuildAction for RunMinilints {
fn command(&self) -> &str {
"$minilints_bin $fix $stamp"
}
fn bypass_runner(&self) -> bool {
true
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("minilints_bin", inputs![":build:minilints"]);
build.add_inputs("", &self.deps);
build.add_variable("fix", if self.fix { "fix" } else { "check" });
build.add_output_stamp(format!("tests/minilints.{}", self.fix));
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
build.add_action(
"build:minilints",
CargoBuild {
inputs: inputs![glob!("tools/minilints/**/*")],
outputs: &[RustOutput::Binary("minilints")],
target: None,
extra_args: "-p minilints",
release_override: Some(BuildProfile::Debug),
},
)
}
}
let files = inputs![
glob![
"**/*.{py,rs,ts,svelte,mjs,md}",
"{node_modules,ts/.svelte-kit}/**"
],
"Cargo.lock"
];
build.add_action(
"check:minilints",
RunMinilints {
deps: files.clone(),
fix: false,
},
)?;
build.add_action(
"fix:minilints",
RunMinilints {
deps: files,
fix: true,
},
)?;
Ok(())
}
================================================
FILE: build/configure/src/web.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use ninja_gen::action::BuildAction;
use ninja_gen::copy::CopyFiles;
use ninja_gen::glob;
use ninja_gen::hashmap;
use ninja_gen::input::BuildInput;
use ninja_gen::inputs;
use ninja_gen::node::node_archive;
use ninja_gen::node::CompileSass;
use ninja_gen::node::DPrint;
use ninja_gen::node::EsbuildScript;
use ninja_gen::node::Eslint;
use ninja_gen::node::GenTypescriptProto;
use ninja_gen::node::Prettier;
use ninja_gen::node::SqlFormat;
use ninja_gen::node::SvelteCheck;
use ninja_gen::node::SveltekitBuild;
use ninja_gen::node::ViteTest;
use ninja_gen::rsync::RsyncFiles;
use ninja_gen::Build;
pub fn build_and_check_web(build: &mut Build) -> Result<()> {
setup_node(build)?;
build_sass(build)?;
build_and_check_tslib(build)?;
build_sveltekit(build)?;
declare_and_check_other_libraries(build)?;
build_and_check_pages(build)?;
build_and_check_editor(build)?;
build_and_check_reviewer(build)?;
build_and_check_mathjax(build)?;
check_web(build)?;
Ok(())
}
fn build_sveltekit(build: &mut Build) -> Result<()> {
build.add_action(
"sveltekit",
SveltekitBuild {
output_folder: inputs!["sveltekit"],
deps: inputs![
"ts/tsconfig.json",
glob!["ts/**", "ts/.svelte-kit/**"],
":ts:lib"
],
},
)
}
fn setup_node(build: &mut Build) -> Result<()> {
ninja_gen::node::setup_node(
build,
node_archive(build.host_platform),
&[
"dprint",
"svelte-check",
"eslint",
"sass",
"tsc",
"tsx",
"vite",
"vitest",
"protoc-gen-es",
"prettier",
],
hashmap! {
"jquery" => vec![
"jquery/dist/jquery.min.js".into()
],
"jquery-ui" => vec![
"jquery-ui-dist/jquery-ui.min.js".into()
],
"bootstrap-dist" => vec![
"bootstrap/dist/js/bootstrap.bundle.min.js".into(),
],
"mathjax" => MATHJAX_FILES.iter().map(|&v| v.into()).collect(),
"mdi_unthemed" => [
// saved searches
"heart-outline.svg",
// today
"clock-outline.svg",
// state
"circle.svg",
"circle-outline.svg",
// flags
"flag-variant.svg",
"flag-variant-outline.svg",
"flag-variant-off-outline.svg",
// decks
"book-outline.svg",
"book-clock-outline.svg",
"book-cog-outline.svg",
// notetypes
"newspaper.svg",
// cardtype
"application-braces-outline.svg",
// fields
"form-textbox.svg",
// tags
"tag-outline.svg",
"tag-off-outline.svg",
].iter().map(|file| format!("@mdi/svg/svg/{file}").into()).collect(),
"mdi_themed" => [
// sidebar tools
"magnify.svg",
"selection-drag.svg",
// QComboBox arrows
"chevron-up.svg",
"chevron-down.svg",
// QHeaderView arrows
"menu-up.svg",
"menu-down.svg",
// drag handle
"drag-vertical.svg",
"drag-horizontal.svg",
// checkbox
"check.svg",
"minus-thick.svg",
// QRadioButton
"circle-medium.svg",
].iter().map(|file| format!("@mdi/svg/svg/{file}").into()).collect(),
},
)?;
Ok(())
}
fn build_and_check_tslib(build: &mut Build) -> Result<()> {
build.add_dependency("ts:generated:i18n", ":rslib:i18n:ts".into());
build.add_action(
"ts:generated:proto",
GenTypescriptProto {
protos: inputs![glob!["proto/**/*.proto"]],
include_dirs: &["proto"],
out_dir: "out/ts/lib/generated",
out_path_transform: |path| {
path.replace("proto/", "ts/lib/generated/")
.replace("proto\\", "ts/lib/generated\\")
},
ts_transform_script: "ts/tools/markpure.ts",
},
)?;
// ensure _service files are generated by rslib
build.add_dependency("ts:generated:proto", inputs![":rslib:proto:ts"]);
// copy source files from ts/lib/generated
build.add_action(
"ts:generated:src",
CopyFiles {
inputs: inputs![glob!["ts/lib/generated/*.ts"]],
output_folder: "ts/lib/generated",
},
)?;
let src_files = inputs![glob!["ts/lib/**"]];
build.add_dependency("ts:lib", inputs![":ts:generated"]);
build.add_dependency("ts:lib", src_files);
Ok(())
}
fn declare_and_check_other_libraries(build: &mut Build) -> Result<()> {
for (library, inputs) in [
("sveltelib", inputs![":ts:lib", glob!("ts/sveltelib/**")]),
("domlib", inputs![":ts:lib", glob!("ts/domlib/**")]),
(
"components",
inputs![":ts:lib", ":ts:sveltelib", glob!("ts/components/**")],
),
("html-filter", inputs![glob!("ts/html-filter/**")]),
] {
let library_with_ts = format!("ts:{library}");
build.add_dependency(&library_with_ts, inputs.clone());
}
Ok(())
}
fn build_and_check_pages(build: &mut Build) -> Result<()> {
let mut build_page = |name: &str, html: bool, deps: BuildInput| -> Result<()> {
let group = format!("ts:{name}");
let deps = inputs![deps, glob!(format!("ts/{name}/**"))];
let extra_exts = if html { &["css", "html"][..] } else { &["css"] };
let entrypoint = if html {
format!("ts/routes/{name}/index.ts")
} else {
format!("ts/{name}/index.ts")
};
build.add_action(
&group,
EsbuildScript {
script: inputs!["ts/bundle_svelte.mjs"],
entrypoint: inputs![entrypoint],
output_stem: &format!("ts/{name}/{name}"),
deps: deps.clone(),
extra_exts,
},
)?;
build.add_dependency("ts:pages", inputs![format!(":{group}")]);
Ok(())
};
// we use the generated .css file separately
build_page(
"editable",
false,
inputs![
//
":ts:lib",
":ts:components",
":ts:domlib",
":ts:sveltelib",
":sass",
":sveltekit",
],
)?;
build_page(
"congrats",
true,
inputs![
//
":ts:lib",
":ts:components",
":sass",
":sveltekit"
],
)?;
Ok(())
}
fn build_and_check_editor(build: &mut Build) -> Result<()> {
let editor_deps = inputs![
//
":ts:lib",
":ts:components",
":ts:domlib",
":ts:sveltelib",
":ts:html-filter",
":sass",
":sveltekit",
glob!("ts/{editable,editor,routes/image-occlusion}/**")
];
build.add_action(
"ts:editor",
EsbuildScript {
script: "ts/bundle_svelte.mjs".into(),
entrypoint: "ts/editor/index.ts".into(),
output_stem: "ts/editor/editor",
deps: editor_deps.clone(),
extra_exts: &["css"],
},
)?;
Ok(())
}
fn build_and_check_reviewer(build: &mut Build) -> Result<()> {
let reviewer_deps = inputs![
":ts:lib",
glob!("ts/{reviewer,image-occlusion}/**"),
":sveltekit"
];
build.add_action(
"ts:reviewer:reviewer.js",
EsbuildScript {
script: inputs!["ts/bundle_ts.mjs"],
entrypoint: "ts/reviewer/index_wrapper.ts".into(),
output_stem: "ts/reviewer/reviewer",
deps: reviewer_deps.clone(),
extra_exts: &[],
},
)?;
build.add_action(
"ts:reviewer:reviewer.css",
CompileSass {
input: inputs!["ts/reviewer/reviewer.scss"],
output: "ts/reviewer/reviewer.css",
deps: inputs![":sass", "ts/routes/image-occlusion/review.scss"],
load_paths: vec!["."],
},
)?;
build.add_action(
"ts:reviewer:reviewer_extras_bundle.js",
EsbuildScript {
script: inputs!["ts/bundle_ts.mjs"],
entrypoint: "ts/reviewer/reviewer_extras.ts".into(),
output_stem: "ts/reviewer/reviewer_extras_bundle",
deps: reviewer_deps.clone(),
extra_exts: &[],
},
)?;
build.add_action(
"ts:reviewer:reviewer_extras.css",
CompileSass {
input: inputs!["ts/reviewer/reviewer_extras.scss"],
output: "ts/reviewer/reviewer_extras.css",
deps: inputs!["ts/routes/image-occlusion/review.scss"],
load_paths: vec!["."],
},
)?;
Ok(())
}
fn check_web(build: &mut Build) -> Result<()> {
let fmt_excluded = "{target,ts/.svelte-kit,node_modules}/**";
let dprint_files = inputs![glob!["**/*.{ts,mjs,js,md,json,toml,scss}", fmt_excluded]];
let prettier_files = inputs![glob!["**/*.svelte", fmt_excluded]];
build.add_action(
"check:format:dprint",
DPrint {
inputs: dprint_files.clone(),
check_only: true,
},
)?;
build.add_action(
"format:dprint",
DPrint {
inputs: dprint_files,
check_only: false,
},
)?;
build.add_action(
"check:format:prettier",
Prettier {
inputs: prettier_files.clone(),
check_only: true,
},
)?;
build.add_action(
"format:prettier",
Prettier {
inputs: prettier_files,
check_only: false,
},
)?;
build.add_action(
"check:vitest",
ViteTest {
deps: inputs![
":node_modules",
":ts:generated",
glob!["ts/{svelte.config.js,vite.config.ts,tsconfig.json}"],
glob!["ts/{lib,deck-options,html-filter,domlib,reviewer,change-notetype}/**/*"],
],
},
)?;
build.add_action(
"check:svelte",
SvelteCheck {
tsconfig: inputs!["ts/tsconfig.json"],
inputs: inputs![
":node_modules",
":ts:generated",
glob!["ts/**/*", "ts/.svelte-kit/**"],
],
},
)?;
let eslint_rc = inputs![".eslintrc.cjs"];
for folder in ["ts", "qt/aqt/data/web/js"] {
let inputs = inputs![glob![format!("{folder}/**"), "ts/.svelte-kit/**"]];
build.add_action(
"check:eslint",
Eslint {
folder,
inputs: inputs.clone(),
eslint_rc: eslint_rc.clone(),
fix: false,
},
)?;
build.add_action(
"fix:eslint",
Eslint {
folder,
inputs,
eslint_rc: eslint_rc.clone(),
fix: true,
},
)?;
}
Ok(())
}
pub fn check_sql(build: &mut Build) -> Result<()> {
build.add_action(
"check:format:sql",
SqlFormat {
inputs: inputs![glob!["**/*.sql"]],
check_only: true,
},
)?;
build.add_action(
"format:sql",
SqlFormat {
inputs: inputs![glob!["**/*.sql"]],
check_only: false,
},
)?;
Ok(())
}
fn build_and_check_mathjax(build: &mut Build) -> Result<()> {
let files = inputs![glob!["ts/mathjax/*"], ":sveltekit"];
build.add_action(
"ts:mathjax",
EsbuildScript {
script: "ts/transform_ts.mjs".into(),
entrypoint: "ts/mathjax/index.ts".into(),
deps: files.clone(),
output_stem: "ts/mathjax/mathjax",
extra_exts: &[],
},
)
}
pub const MATHJAX_FILES: &[&str] = &[
"mathjax/es5/a11y/assistive-mml.js",
"mathjax/es5/a11y/complexity.js",
"mathjax/es5/a11y/explorer.js",
"mathjax/es5/a11y/semantic-enrich.js",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_AMS-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Calligraphic-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Fraktur-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Main-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-BoldItalic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Math-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Italic.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_SansSerif-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Script-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size1-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size2-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size3-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Size4-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Typewriter-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Bold.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Vector-Regular.woff",
"mathjax/es5/output/chtml/fonts/woff-v2/MathJax_Zero.woff",
"mathjax/es5/tex-chtml-full.js",
"mathjax/es5/sre/mathmaps/de.json",
"mathjax/es5/sre/mathmaps/en.json",
"mathjax/es5/sre/mathmaps/es.json",
"mathjax/es5/sre/mathmaps/fr.json",
"mathjax/es5/sre/mathmaps/hi.json",
"mathjax/es5/sre/mathmaps/it.json",
"mathjax/es5/sre/mathmaps/nemeth.json",
];
pub fn copy_mathjax() -> impl BuildAction {
RsyncFiles {
inputs: inputs![":node_modules:mathjax"],
target_folder: "qt/_aqt/data/web/js/vendor/mathjax",
strip_prefix: "$builddir/node_modules/mathjax/es5",
extra_args: "",
}
}
fn build_sass(build: &mut Build) -> Result<()> {
build.add_dependency("sass", inputs![glob!("ts/lib/sass/**")]);
build.add_action(
"css:_root-vars",
CompileSass {
input: inputs!["ts/lib/sass/_root-vars.scss"],
output: "ts/lib/sass/_root-vars.css",
deps: inputs![glob!["ts/lib/sass/*"]],
load_paths: vec![],
},
)?;
Ok(())
}
================================================
FILE: build/ninja_gen/Cargo.toml
================================================
[package]
name = "ninja_gen"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
publish = false
rust-version.workspace = true
[dependencies]
anki_io.workspace = true
anyhow.workspace = true
camino.workspace = true
dunce.workspace = true
globset.workspace = true
itertools.workspace = true
maplit.workspace = true
num_cpus.workspace = true
regex.workspace = true
serde_json.workspace = true
sha2.workspace = true
walkdir.workspace = true
which.workspace = true
[target.'cfg(windows)'.dependencies]
reqwest = { workspace = true, features = ["blocking", "json", "native-tls"] }
[target.'cfg(not(windows))'.dependencies]
reqwest = { workspace = true, features = ["blocking", "json", "rustls-tls"] }
[[bin]]
name = "update_uv"
path = "src/bin/update_uv.rs"
[[bin]]
name = "update_protoc"
path = "src/bin/update_protoc.rs"
[[bin]]
name = "update_node"
path = "src/bin/update_node.rs"
================================================
FILE: build/ninja_gen/src/action.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use anyhow::Result;
use crate::build::FilesHandle;
use crate::Build;
pub trait BuildAction {
/// Command line to invoke for each build statement.
fn command(&self) -> &str;
/// Declare the input files and variables, and output files.
fn files(&mut self, build: &mut impl FilesHandle);
/// If true, this action will not trigger a rebuild of dependent targets if
/// the output files are unchanged. This corresponds to Ninja's "restat"
/// argument.
fn check_output_timestamps(&self) -> bool {
false
}
/// True if this rule generates build.ninja
fn generator(&self) -> bool {
false
}
/// Called on first action invocation; can be used to inject other build
/// actions to perform initial setup.
#[allow(unused_variables)]
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
Ok(())
}
fn concurrency_pool(&self) -> Option<&'static str> {
None
}
fn bypass_runner(&self) -> bool {
false
}
fn hide_success(&self) -> bool {
true
}
fn hide_progress(&self) -> bool {
false
}
fn name(&self) -> &'static str {
std::any::type_name::()
.split("::")
.last()
.unwrap()
.split('<')
.next()
.unwrap()
}
}
#[cfg(test)]
trait TestBuildAction {}
#[cfg(test)]
impl BuildAction for T {
fn command(&self) -> &str {
"test"
}
fn files(&mut self, _build: &mut impl FilesHandle) {}
}
#[allow(dead_code, unused_variables)]
#[test]
fn should_strip_regions_in_type_name() {
struct Bare;
impl TestBuildAction for Bare {}
assert_eq!(Bare {}.name(), "Bare");
struct WithLifeTime<'a>(&'a str);
impl TestBuildAction for WithLifeTime<'_> {}
assert_eq!(WithLifeTime("test").name(), "WithLifeTime");
struct WithMultiLifeTime<'a, 'b>(&'a str, &'b str);
impl TestBuildAction for WithMultiLifeTime<'_, '_> {}
assert_eq!(
WithMultiLifeTime("test", "test").name(),
"WithMultiLifeTime"
);
struct WithGeneric(T);
impl TestBuildAction for WithGeneric {}
assert_eq!(WithGeneric(3).name(), "WithGeneric");
}
================================================
FILE: build/ninja_gen/src/archives.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::borrow::Cow;
use std::collections::HashMap;
use anyhow::Result;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use crate::action::BuildAction;
use crate::input::BuildInput;
use crate::inputs;
use crate::Build;
#[derive(Clone, Copy, Debug)]
pub struct OnlineArchive {
pub url: &'static str,
pub sha256: &'static str,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Platform {
LinuxX64,
LinuxArm,
MacX64,
MacArm,
WindowsX64,
WindowsArm,
}
impl Platform {
pub fn current() -> Self {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
match (os, arch) {
("linux", "x86_64") => Self::LinuxX64,
("linux", "aarch64") => Self::LinuxArm,
("macos", "x86_64") => Self::MacX64,
("macos", "aarch64") => Self::MacArm,
("windows", "x86_64") => Self::WindowsX64,
("windows", "aarch64") => Self::WindowsArm,
_ => panic!("unsupported os/arch {os} {arch} - PR welcome!"),
}
}
pub fn tls_feature() -> &'static str {
match Self::current() {
// On Linux, wheels are not allowed to link to OpenSSL, and linking setup
// caused pain for AnkiDroid in the past. On other platforms, we stick to
// native libraries, for smaller binaries.
Platform::LinuxX64 | Platform::LinuxArm => "rustls",
_ => "native-tls",
}
}
pub fn as_rust_triple(&self) -> &'static str {
match self {
Platform::LinuxX64 => "x86_64-unknown-linux-gnu",
Platform::LinuxArm => "aarch64-unknown-linux-gnu",
Platform::MacX64 => "x86_64-apple-darwin",
Platform::MacArm => "aarch64-apple-darwin",
Platform::WindowsX64 => "x86_64-pc-windows-msvc",
Platform::WindowsArm => "aarch64-pc-windows-msvc",
}
}
}
/// Append .exe to path if on Windows.
pub fn with_exe(path: &str) -> Cow<'_, str> {
if cfg!(windows) {
format!("{path}.exe").into()
} else {
path.into()
}
}
struct DownloadArchive {
pub archive: OnlineArchive,
}
impl BuildAction for DownloadArchive {
fn command(&self) -> &str {
"$runner archive download $url $checksum $out"
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
let (_, filename) = self.archive.url.rsplit_once('/').unwrap();
let output_path = Utf8Path::new("download").join(filename);
build.add_variable("url", self.archive.url);
build.add_variable("checksum", self.archive.sha256);
build.add_outputs("out", &[output_path.into_string()])
}
fn check_output_timestamps(&self) -> bool {
true
}
}
struct ExtractArchive<'a, I> {
pub archive_path: BuildInput,
/// The folder that the archive should be extracted into, relative to
/// $builddir/extracted. If the archive contains a single top-level
/// folder, its contents will be extracted into the provided folder, so
/// that output like tool-1.2/ can be extracted into tool/.
pub extraction_folder_name: &'a str,
/// Files contained inside the archive, relative to the archive root, and
/// excluding the top-level folder if it is the sole top-level entry.
/// Any files you wish to use as part of subsequent rules
/// must be declared here.
pub file_manifest: HashMap<&'static str, I>,
}
impl ExtractArchive<'_, I> {
fn extraction_folder(&self) -> Utf8PathBuf {
Utf8Path::new("$builddir")
.join("extracted")
.join(self.extraction_folder_name)
}
}
impl BuildAction for ExtractArchive<'_, I>
where
I: IntoIterator,
I::Item: AsRef,
{
fn command(&self) -> &str {
"$runner archive extract $in $extraction_folder"
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
build.add_inputs("in", inputs![self.archive_path.clone()]);
let folder = self.extraction_folder();
build.add_variable("extraction_folder", folder.to_string());
for (subgroup, files) in self.file_manifest.drain() {
build.add_outputs_ext(
subgroup,
files
.into_iter()
.map(|f| folder.join(f.as_ref()).to_string()),
!subgroup.is_empty(),
);
}
build.add_output_stamp(folder.with_extension("marker"));
}
fn name(&self) -> &'static str {
"extract"
}
fn check_output_timestamps(&self) -> bool {
true
}
}
/// See [DownloadArchive] and [ExtractArchive].
pub fn download_and_extract(
build: &mut Build,
group_name: &str,
archive: OnlineArchive,
file_manifest: HashMap<&'static str, I>,
) -> Result<()>
where
I: IntoIterator,
I::Item: AsRef,
{
let download_group = format!("download:{group_name}");
build.add_action(&download_group, DownloadArchive { archive })?;
let extract_group = format!("extract:{group_name}");
build.add_action(
extract_group,
ExtractArchive {
archive_path: inputs![format!(":{download_group}")],
extraction_folder_name: group_name,
file_manifest,
},
)?;
Ok(())
}
pub fn empty_manifest() -> HashMap<&'static str, &'static [&'static str]> {
Default::default()
}
================================================
FILE: build/ninja_gen/src/bin/update_node.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::error::Error;
use std::fs;
use std::path::Path;
use regex::Regex;
use reqwest::blocking::Client;
use serde_json::Value;
#[derive(Debug)]
struct NodeRelease {
version: String,
files: Vec,
}
#[derive(Debug)]
struct NodeFile {
filename: String,
url: String,
}
fn main() -> Result<(), Box> {
let release_info = fetch_node_release_info()?;
let new_text = generate_node_archive_function(&release_info)?;
update_node_text(&new_text)?;
println!("Node.js archive function updated successfully!");
Ok(())
}
fn fetch_node_release_info() -> Result> {
let client = Client::new();
// Get the Node.js release info
let response = client
.get("https://nodejs.org/dist/index.json")
.header("User-Agent", "anki-build-updater")
.send()?;
let releases: Vec = response.json()?;
// Find the latest LTS release
let latest = releases
.iter()
.find(|release| {
// LTS releases have a non-false "lts" field
release["lts"].as_str().is_some() && release["lts"] != false
})
.ok_or("No LTS releases found")?;
let version = latest["version"]
.as_str()
.ok_or("Version not found")?
.to_string();
let files = latest["files"]
.as_array()
.ok_or("Files array not found")?
.iter()
.map(|f| f.as_str().unwrap_or(""))
.collect::>();
let lts_name = latest["lts"].as_str().unwrap_or("unknown");
println!("Found Node.js LTS version: {version} ({lts_name})");
// Map platforms to their expected file keys and full filenames
let platform_mapping = vec![
(
"linux-x64",
"linux-x64",
format!("node-{version}-linux-x64.tar.xz"),
),
(
"linux-arm64",
"linux-arm64",
format!("node-{version}-linux-arm64.tar.xz"),
),
(
"darwin-x64",
"osx-x64-tar",
format!("node-{version}-darwin-x64.tar.xz"),
),
(
"darwin-arm64",
"osx-arm64-tar",
format!("node-{version}-darwin-arm64.tar.xz"),
),
(
"win-x64",
"win-x64-zip",
format!("node-{version}-win-x64.zip"),
),
(
"win-arm64",
"win-arm64-zip",
format!("node-{version}-win-arm64.zip"),
),
];
let mut node_files = Vec::new();
for (platform, file_key, filename) in platform_mapping {
// Check if this file exists in the release
if files.contains(&file_key) {
let url = format!("https://nodejs.org/dist/{version}/{filename}");
node_files.push(NodeFile {
filename: filename.clone(),
url,
});
println!("Found file for {platform}: {filename} (key: {file_key})");
} else {
return Err(
format!("File not found for {platform} (key: {file_key}): {filename}").into(),
);
}
}
Ok(NodeRelease {
version,
files: node_files,
})
}
fn generate_node_archive_function(release: &NodeRelease) -> Result> {
let client = Client::new();
// Fetch the SHASUMS256.txt file once
println!("Fetching SHA256 checksums...");
let shasums_url = format!("https://nodejs.org/dist/{}/SHASUMS256.txt", release.version);
let shasums_response = client
.get(&shasums_url)
.header("User-Agent", "anki-build-updater")
.send()?;
let shasums_text = shasums_response.text()?;
// Create a mapping from filename patterns to platform names - using the exact
// patterns we stored in files
let platform_mapping = vec![
("linux-x64.tar.xz", "LinuxX64"),
("linux-arm64.tar.xz", "LinuxArm"),
("darwin-x64.tar.xz", "MacX64"),
("darwin-arm64.tar.xz", "MacArm"),
("win-x64.zip", "WindowsX64"),
("win-arm64.zip", "WindowsArm"),
];
let mut platform_blocks = Vec::new();
for (file_pattern, platform_name) in platform_mapping {
// Find the file that ends with this pattern
if let Some(file) = release
.files
.iter()
.find(|f| f.filename.ends_with(file_pattern))
{
// Find the SHA256 for this file
let sha256 = shasums_text
.lines()
.find(|line| line.contains(&file.filename))
.and_then(|line| line.split_whitespace().next())
.ok_or_else(|| format!("SHA256 not found for {}", file.filename))?;
println!(
"Found SHA256 for {}: {} => {}",
platform_name, file.filename, sha256
);
let block = format!(
" Platform::{} => OnlineArchive {{\n url: \"{}\",\n sha256: \"{}\",\n }},",
platform_name, file.url, sha256
);
platform_blocks.push(block);
} else {
return Err(format!(
"File not found for platform {platform_name}: no file ending with {file_pattern}"
)
.into());
}
}
let function = format!(
"pub fn node_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}\n}}",
platform_blocks.join("\n")
);
Ok(function)
}
fn update_node_text(new_function: &str) -> Result<(), Box> {
let node_rs_content = read_node_rs()?;
// Regex to match the entire node_archive function with proper multiline
// matching
let re = Regex::new(
r"(?s)pub fn node_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}",
)?;
let updated_content = re.replace(&node_rs_content, new_function);
write_node_rs(&updated_content)?;
Ok(())
}
fn read_node_rs() -> Result> {
// Use CARGO_MANIFEST_DIR to get the crate root, then find src/node.rs
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set")?;
let path = Path::new(&manifest_dir).join("src").join("node.rs");
Ok(fs::read_to_string(path)?)
}
fn write_node_rs(content: &str) -> Result<(), Box> {
// Use CARGO_MANIFEST_DIR to get the crate root, then find src/node.rs
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR not set")?;
let path = Path::new(&manifest_dir).join("src").join("node.rs");
fs::write(path, content)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regex_replacement() {
let sample_content = r#"Some other code
pub fn node_archive(platform: Platform) -> OnlineArchive {
match platform {
Platform::LinuxX64 => OnlineArchive {
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz",
sha256: "old_hash",
},
Platform::MacX64 => OnlineArchive {
url: "https://nodejs.org/dist/v20.11.0/node-v20.11.0-darwin-x64.tar.xz",
sha256: "old_hash",
},
}
}
More code here"#;
let new_function = r#"pub fn node_archive(platform: Platform) -> OnlineArchive {
match platform {
Platform::LinuxX64 => OnlineArchive {
url: "https://nodejs.org/dist/v21.0.0/node-v21.0.0-linux-x64.tar.xz",
sha256: "new_hash",
},
Platform::MacX64 => OnlineArchive {
url: "https://nodejs.org/dist/v21.0.0/node-v21.0.0-darwin-x64.tar.xz",
sha256: "new_hash",
},
}
}"#;
let re = Regex::new(
r"(?s)pub fn node_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}"
).unwrap();
let result = re.replace(sample_content, new_function);
assert!(result.contains("v21.0.0"));
assert!(result.contains("new_hash"));
assert!(!result.contains("old_hash"));
assert!(result.contains("Some other code"));
assert!(result.contains("More code here"));
}
}
================================================
FILE: build/ninja_gen/src/bin/update_protoc.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::error::Error;
use std::fs;
use std::path::Path;
use regex::Regex;
use reqwest::blocking::Client;
use serde_json::Value;
use sha2::Digest;
use sha2::Sha256;
fn fetch_protoc_release_info() -> Result> {
let client = Client::new();
println!("Fetching latest protoc release info from GitHub...");
// Fetch latest release info
let response = client
.get("https://api.github.com/repos/protocolbuffers/protobuf/releases/latest")
.header("User-Agent", "Anki-Build-Script")
.send()?;
let release_info: Value = response.json()?;
let assets = release_info["assets"]
.as_array()
.expect("assets should be an array");
// Map platform names to their corresponding asset patterns
let platform_patterns = [
("LinuxX64", "linux-x86_64"),
("LinuxArm", "linux-aarch_64"),
("MacX64", "osx-universal_binary"), // Mac uses universal binary for both
("MacArm", "osx-universal_binary"),
("WindowsX64", "win64"), // Windows uses x86 binary for both archs
("WindowsArm", "win64"),
];
let mut match_blocks = Vec::new();
for (platform, pattern) in platform_patterns {
// Find the asset matching the platform pattern
let asset = assets.iter().find(|asset| {
let name = asset["name"].as_str().unwrap_or("");
name.starts_with("protoc-") && name.contains(pattern) && name.ends_with(".zip")
});
if asset.is_none() {
eprintln!("No asset found for platform {platform} pattern {pattern}");
continue;
}
let asset = asset.unwrap();
let download_url = asset["browser_download_url"].as_str().unwrap();
let asset_name = asset["name"].as_str().unwrap();
// Download the file and calculate SHA256 locally
println!("Downloading and checksumming {asset_name} for {platform}...");
let response = client
.get(download_url)
.header("User-Agent", "Anki-Build-Script")
.send()?;
let bytes = response.bytes()?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let sha256 = format!("{:x}", hasher.finalize());
// Handle platform-specific match patterns
let match_pattern = match platform {
"MacX64" => "Platform::MacX64 | Platform::MacArm",
"MacArm" => continue, // Skip MacArm since it's handled with MacX64
"WindowsX64" => "Platform::WindowsX64 | Platform::WindowsArm",
"WindowsArm" => continue, // Skip WindowsArm since it's handled with WindowsX64
_ => &format!("Platform::{platform}"),
};
match_blocks.push(format!(
" {match_pattern} => {{\n OnlineArchive {{\n url: \"{download_url}\",\n sha256: \"{sha256}\",\n }}\n }}"
));
}
Ok(format!(
"pub fn protoc_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}\n}}",
match_blocks.join(",\n")
))
}
fn read_protobuf_rs() -> Result> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/protobuf.rs");
println!("Reading {}", path.display());
let content = fs::read_to_string(path)?;
Ok(content)
}
fn update_protoc_text(old_text: &str, new_protoc_text: &str) -> Result> {
let re =
Regex::new(r"(?ms)^pub fn protoc_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\}")
.unwrap();
if !re.is_match(old_text) {
return Err("Could not find protoc_archive function block to replace".into());
}
let new_content = re.replace(old_text, new_protoc_text).to_string();
println!("Original lines: {}", old_text.lines().count());
println!("Updated lines: {}", new_content.lines().count());
Ok(new_content)
}
fn write_protobuf_rs(content: &str) -> Result<(), Box> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/protobuf.rs");
println!("Writing to {}", path.display());
fs::write(path, content)?;
Ok(())
}
fn main() -> Result<(), Box> {
let new_protoc_archive = fetch_protoc_release_info()?;
let content = read_protobuf_rs()?;
let updated_content = update_protoc_text(&content, &new_protoc_archive)?;
write_protobuf_rs(&updated_content)?;
println!("Successfully updated protoc_archive function in protobuf.rs");
Ok(())
}
================================================
FILE: build/ninja_gen/src/bin/update_uv.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::error::Error;
use std::fs;
use std::path::Path;
use regex::Regex;
use reqwest::blocking::Client;
use serde_json::Value;
fn fetch_uv_release_info() -> Result> {
let client = Client::new();
println!("Fetching latest uv release info from GitHub...");
// Fetch latest release info
let response = client
.get("https://api.github.com/repos/astral-sh/uv/releases/latest")
.header("User-Agent", "Anki-Build-Script")
.send()?;
let release_info: Value = response.json()?;
let assets = release_info["assets"]
.as_array()
.expect("assets should be an array");
// Map platform names to their corresponding asset patterns
let platform_patterns = [
("LinuxX64", "x86_64-unknown-linux-gnu"),
("LinuxArm", "aarch64-unknown-linux-gnu"),
("MacX64", "x86_64-apple-darwin"),
("MacArm", "aarch64-apple-darwin"),
("WindowsX64", "x86_64-pc-windows-msvc"),
("WindowsArm", "aarch64-pc-windows-msvc"),
];
let mut match_blocks = Vec::new();
for (platform, pattern) in platform_patterns {
// Find the asset matching the platform pattern (the binary)
let asset = assets.iter().find(|asset| {
let name = asset["name"].as_str().unwrap_or("");
name.contains(pattern) && (name.ends_with(".tar.gz") || name.ends_with(".zip"))
});
if asset.is_none() {
eprintln!("No asset found for platform {platform} pattern {pattern}");
continue;
}
let asset = asset.unwrap();
let download_url = asset["browser_download_url"].as_str().unwrap();
let asset_name = asset["name"].as_str().unwrap();
// Find the corresponding .sha256 or .sha256sum asset
let sha_asset = assets.iter().find(|a| {
let name = a["name"].as_str().unwrap_or("");
name == format!("{asset_name}.sha256") || name == format!("{asset_name}.sha256sum")
});
if sha_asset.is_none() {
eprintln!("No sha256 asset found for {asset_name}");
continue;
}
let sha_asset = sha_asset.unwrap();
let sha_url = sha_asset["browser_download_url"].as_str().unwrap();
println!("Fetching SHA256 for {platform}...");
let sha_text = client
.get(sha_url)
.header("User-Agent", "Anki-Build-Script")
.send()?
.text()?;
// The sha file is usually of the form: " "
let sha256 = sha_text.split_whitespace().next().unwrap_or("");
match_blocks.push(format!(
" Platform::{platform} => {{\n OnlineArchive {{\n url: \"{download_url}\",\n sha256: \"{sha256}\",\n }}\n }}"
));
}
Ok(format!(
"pub fn uv_archive(platform: Platform) -> OnlineArchive {{\n match platform {{\n{}\n }}",
match_blocks.join(",\n")
))
}
fn read_python_rs() -> Result> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/python.rs");
println!("Reading {}", path.display());
let content = fs::read_to_string(path)?;
Ok(content)
}
fn update_uv_text(old_text: &str, new_uv_text: &str) -> Result> {
let re = Regex::new(r"(?ms)^pub fn uv_archive\(platform: Platform\) -> OnlineArchive \{.*?\n\s*\}\s*\n\s*\}\s*\n\s*\}").unwrap();
if !re.is_match(old_text) {
return Err("Could not find uv_archive function block to replace".into());
}
let new_content = re.replace(old_text, new_uv_text).to_string();
println!("Original lines: {}", old_text.lines().count());
println!("Updated lines: {}", new_content.lines().count());
Ok(new_content)
}
fn write_python_rs(content: &str) -> Result<(), Box> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = Path::new(&manifest_dir).join("src/python.rs");
println!("Writing to {}", path.display());
fs::write(path, content)?;
Ok(())
}
fn main() -> Result<(), Box> {
let new_uv_archive = fetch_uv_release_info()?;
let content = read_python_rs()?;
let updated_content = update_uv_text(&content, &new_uv_archive)?;
write_python_rs(&updated_content)?;
println!("Successfully updated uv_archive function in python.rs");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_uv_text_with_actual_file() {
let content = fs::read_to_string("src/python.rs").unwrap();
let original_lines = content.lines().count();
const EXPECTED_LINES_REMOVED: usize = 38;
let updated = update_uv_text(&content, "").unwrap();
let updated_lines = updated.lines().count();
assert_eq!(
updated_lines,
original_lines - EXPECTED_LINES_REMOVED,
"Expected line count to decrease by exactly {EXPECTED_LINES_REMOVED} lines (original: {original_lines}, updated: {updated_lines})"
);
}
}
================================================
FILE: build/ninja_gen/src/build.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Write;
use anyhow::Result;
use camino::Utf8PathBuf;
use itertools::Itertools;
use crate::action::BuildAction;
use crate::archives::Platform;
use crate::configure::ConfigureBuild;
use crate::input::space_separated;
use crate::input::BuildInput;
#[derive(Debug)]
pub struct Build {
pub variables: HashMap<&'static str, String>,
pub buildroot: Utf8PathBuf,
pub build_profile: BuildProfile,
pub pools: Vec<(&'static str, usize)>,
pub trailing_text: String,
pub host_platform: Platform,
pub have_n2: bool,
pub(crate) output_text: String,
action_names: HashSet<&'static str>,
pub(crate) groups: HashMap>,
}
impl Build {
pub fn new() -> Result {
let buildroot = if cfg!(windows) {
Utf8PathBuf::from("out")
} else {
// on Unix systems we allow out to be a symlink to an external location
Utf8PathBuf::from("out").canonicalize_utf8()?
};
let mut build = Build {
buildroot,
build_profile: BuildProfile::from_env(),
host_platform: Platform::current(),
variables: Default::default(),
pools: Default::default(),
trailing_text: Default::default(),
output_text: Default::default(),
action_names: Default::default(),
groups: Default::default(),
have_n2: which::which("n2").is_ok(),
};
build.add_action("build:configure", ConfigureBuild {})?;
Ok(build)
}
pub fn variable(&mut self, name: &'static str, value: impl Into) {
self.variables.insert(name, value.into());
}
pub fn pool(&mut self, name: &'static str, size: usize) {
self.pools.push((name, size));
}
/// Evaluate the provided closure only once, using `key` to determine
/// uniqueness. This key should not match any build action name.
pub fn once_only(
&mut self,
key: &'static str,
block: impl FnOnce(&mut Build) -> Result<()>,
) -> Result<()> {
if self.action_names.insert(key) {
block(self)
} else {
Ok(())
}
}
pub fn add_action(&mut self, group: impl AsRef