Showing preview only (6,720K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/out/pylib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/pylib" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/qt" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/extra" />
<excludeFolder url="file://$MODULE_DIR$/out/pyenv" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
================================================
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. <https://www.amboss.com/>
Aristotelis P. <https://glutanimate.com/contact>
Erez Volk <erez.volk@gmail.com>
zjosua <zjosua@hotmail.com>
Yngve Hoiseth <yngve@hoiseth.net>
Arthur Milchior <arthur@milchior.fr>
Ijgnd
Yoonchae Lee <bluegreenmagick@gmail.com>
Evandro Coan <github.com/evandrocoan>
Alan Du <alanhdu@gmail.com>
Yuchen Lei <lyc@xuming.studio>
Henry Tang <hktang@ualberta.ca>
Simone Gaiarin <simgunz@gmail.com>
Rai (Michal Pokorny) <agentydragon@gmail.com>
Zeno Gantner <zeno.gantner@gmail.com>
Henrik Giesel <hengiesel@gmail.com>
Michał Bartoszkiewicz <mbartoszkiewicz@gmail.com>
Sander Santema <github.com/sandersantema/>
Thomas Brownback <https://github.com/brownbat/>
Andrew Gaul <andrew@gaul.org>
kenden
Emil Hamrin <github.com/e-hamrin>
Nickolay Yudin <kelciour@gmail.com>
neitrinoweb <github.com/neitrinoweb/>
Andreas Reis <github.com/nwwt>
Matt Krump <github.com/mkrump>
Alexander Presnyakov <flagist0@gmail.com>
Abdo <github.com/abdnh>
aplaice <plaice.adam+github@gmail.com>
phwoo <github.com/phwoo>
Soren Bjornstad <anki@sorenbjornstad.com>
Aleksa Sarai <cyphar@cyphar.com>
Jakub Kaczmarzyk <jakub.kaczmarzyk@gmail.com>
Akshara Balachandra <akshara.bala.18@gmail.com>
lukkea <github.com/lukkea/>
David Allison <davidallisongithub@gmail.com>
David Allison <62114487+david-allison@users.noreply.github.com>
Tsung-Han Yu <johan456789@gmail.com>
Piotr Kubowicz <piotr.kubowicz@gmail.com>
RumovZ <gp5glkw78@relay.firefox.com>
Cecini <github.com/cecini>
Krish Shah <github.com/k12ish>
ianki <iankigit@gmail.com>
rye761 <ryebread761@gmail.com>
Guillem Palau Salvà <guillempalausalva@gmail.com>
Meredith Derecho <meredithderecho@gmail.com>
Daniel Wallgren <github.com/wallgrenen>
Kerrick Staley <kerrick@kerrickstaley.com>
Maksim Abramchuk <maximabramchuck@gmail.com>
Benjamin Kulnik <benjamin.kulnik@student.tuwien.ac.at>
Shaun Ren <shaun.ren@linux.com>
Ryan Greenblatt <greenblattryan@gmail.com>
Matthias Metelka <github.com/kleinerpirat>
qubist-pixel-ux <github.com/qubist-pixel-ux>
cherryblossom <github.com/cherryblossom000>
Hikaru Yoshiga <github.com/hikaru-y/>
Thore Tyborski <github.com/ThoreBor>
Alexander Giorev <alex.giorev@gmail.com>
Ren Tatsumoto <tatsu@autistici.org>
lolilolicon <lolilolicon@gmail.com>
Gesa Stupperich <gesa.stupperich@gmail.com>
git9527 <github.com/git9527>
Vova Selin <vselin12@gmail.com>
qxo <49526356@qq.com>
Spooghetti420 <github.com/spooghetti420>
Danish Prakash <github.com/danishprakash>
Araceli Yanez <github.com/aracelix>
Sam Bradshaw <samjr.bradshaw@gmail.com>
gnnoh <gerongfenh@gmail.com>
Sachin Govind <sachin.govind.too@gmail.com>
Bruce Harris <github.com/bruceharris>
Patric Cunha <patricc@agap2.pt>
Brayan Oliveira <69634269+BrayanDSO@users.noreply.github.com>
Luka Warren <github.com/lukawarren>
wisherhxl <wisherhxl@gmail.com>
dobefore <1432338032@qq.com>
Bart Louwers <bart.git@emeel.net>
Sam Penny <github.com/sam1penny>
Yutsuten <mateus.etto@gmail.com>
Zoom <zoomrmc+git@gmail.com>
TRIAEIOU <github.com/TRIAEIOU>
Stefan Kangas <stefankangas@gmail.com>
Fabricio Duarte <fabricio.duarte.jr@gmail.com>
Mani <github.com/krmanik>
Kaben Nanlohy <kaben.nanlohy@gmail.com>
Tobias Predel <tobias.predel@gmail.com>
Daniel Tang <danielzgtg.opensource@gmail.com>
Jack Pearson <github.com/jrpear>
yellowjello <github.com/yellowjello>
Ingemar Berg <github.com/ingemarberg>
Ben Kerman <ben@kermanic.org>
Euan Kemp <euank@euank.com>
Kieran Black <kieranlblack@gmail.com>
XeR <github.com/XeR>
mgrottenthaler <github.com/mgrottenthaler>
Austin Siew <github.com/Aquafina-water-bottle>
Joel Koen <mail@joelkoen.com>
Christopher Woggon <christopher.woggon@gmail.com>
Kavel Rao <github.com/kavelrao>
Ben Yip <github.com/bennyyip>
mmjang <752515918@qq.com>
shunlog <github.com/shunlog>
3ter <github.com/3ter>
Derek Dang <github.com/derekdang/>
Luc Mcgrady <github.com/Luc-Mcgrady>
Kehinde Adeleke <adelekekehinde06@gmail.com>
Marko Juhanne <github.com/mjuhanne>
Gabriel Heinatz <anorot@gmail.com>
Monty Evans <montyevans@gmail.com>
Nil Admirari <https://github.com/nihil-admirari>
Michael Winkworth <github.com/SteelColossus>
Mateusz Wojewoda <kawa1.11@o2.pl>
Jarrett Ye <jarrett.ye@outlook.com>
Sam Waechter <github.com/swektr>
Michael Eliachevitch <m.eliachevitch@posteo.de>
Carlo Quick <https://github.com/CarloQuick>
Dominique Martinet <asmadeus@codewreck.org>
chandraiyengar <github.com/chandraiyengar>
user1823 <92206575+user1823@users.noreply.github.com>
Gustaf Carefall <https://github.com/Gustaf-C>
virinci <github.com/virinci>
snowtimeglass <snowtimeglass@gmail.com>
brishtibheja <136738526+brishtibheja@users.noreply.github.com>
Ben Olson <github.com/grepgrok>
Akash Reddy <akashreddy2003@gmail.com>
Lucio Sauer <watermanpaint@posteo.net>
Gustavo Sales <gustavosmendes14@gmail.com>
Shawn M Moore <https://github.com/sartak>
Marko Sisovic <msisovic13@gmail.com>
Viktor Ricci <ricci@primateer.de>
Harvey Randall <harveyrandall2001@gmail.com>
Pedro Lameiras <pedrolameiras@tecnico.ulisboa.pt>
Kai Knoblich <kai@FreeBSD.org>
Lucas Scharenbroch <lucasscharenbroch@gmail.com>
Antonio Cavallo <a.cavallo@cavallinux.eu>
Han Yeong-woo <han@yeongwoo.dev>
Jean Khawand <jk@jeankhawand.com>
Pedro Schreiber <schreiber.mmb@gmail.com>
Foxy_null <https://github.com/Foxy-null>
Arbyste <arbyste@outlook.com>
Vasll <github.com/vasll>
laalsaas <laalsaas@systemli.org>
ijqq <ijqq@protonmail.ch>
AntoineQ1 <https://github.com/AntoineQ1>
jthulhu <https://github.com/jthulhu>
Escape0707 <tothesong@gmail.com>
Loudwig <https://github.com/Loudwig>
Wu Yi-Wei <https://github.com/Ianwu0812>
RRomeroJr <117.rromero@gmail.com>
Xidorn Quan <me@upsuper.org>
Alexander Bocken <alexander@bocken.org>
James Elmore <email@jameselmore.org>
Ian Samir Yep Manzano <https://github.com/isym444>
David Culley <6276049+davidculley@users.noreply.github.com>
Rastislav Kish <rastislav.kish@protonmail.com>
jake <jake@sharnoth.com>
Expertium <https://github.com/Expertium>
Christian Donat <https://github.com/cdonat2>
Asuka Minato <https://asukaminato.eu.org>
Dillon Baldwin <https://github.com/DillBal>
Voczi <https://github.com/voczi>
Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com>
Themis Demetriades <themis100@outlook.com>
Luke Bartholomew <lukesbart@icloud.com>
Gregory Abrasaldo <degeemon@gmail.com>
Taylor Obyen <162023405+taylorobyen@users.noreply.github.com>
Kris Cherven <krischerven@gmail.com>
twwn <github.com/twwn>
Cy Pokhrel <cy@cy7.sh>
Park Hyunwoo <phu54321@naver.com>
Tomas Fabrizio Orsi <torsi@fi.uba.ar>
Dongjin Ouyang <1113117424@qq.com>
Sawan Sunar <sawansunar24072002@gmail.com>
hideo aoyama <https://github.com/boukendesho>
Ross Brown <rbrownwsws@googlemail.com>
🦙 <gh@siid.sh>
Lukas Sommer <sommerluk@gmail.com>
Luca Auer <lolle2000.la@gmail.com>
Niclas Heinz <nheinz@hpost.net>
Omar Kohl <omarkohl@posteo.net>
David Elizalde <david.elizalde.r.a@gmail.com>
beyondcompute <beyondcompute@gmail.com>
Yuki <https://github.com/YukiNagat0>
wackbyte <wackbyte@protonmail.com>
GithubAnon0000 <GithubAnon0000@users.noreply.github.com>
Mike Hardy <github@mikehardy.net>
Danika_Dakika <https://github.com/Danika-Dakika>
Mumtaz Hajjo Alrifai <mumtazrifai@protonmail.com>
Thomas Graves <fate@hey.com>
Jakub Fidler <jakub.fidler@protonmail.com>
Valerie Enfys <val@unidentified.systems>
Julien Chol <https://github.com/chel-ou>
ikkz <ylei.mk@gmail.com>
derivativeoflog7 <https://github.com/derivativeoflog7>
rreemmii-dev <https://github.com/rreemmii-dev>
babofitos <https://github.com/babofitos>
Jonathan Schoreels <https://github.com/JSchoreels>
JL710
Matt Brubeck <mbrubeck@limpet.net>
Yaoliang Chen <yaoliang.ch@gmail.com>
KolbyML <https://github.com/KolbyML>
Adnane Taghi <dev@soleuniverse.me>
Spiritual Father <https://github.com/spiritualfather>
Emmanuel Ferdman <https://github.com/emmanuel-ferdman>
Sunong2008 <https://github.com/Sunrongguo2008>
Marvin Kopf <marvinkopf@outlook.com>
Kevin Nakamura <grinkers@grinkers.net>
Bradley Szoke <bradleyszoke@gmail.com>
jcznk <https://github.com/jcznk>
Thomas Rixen <thomas.rixen@student.uclouvain.be>
Siyuan Mattuwu Yan <syan4@ualberta.ca>
Lee Doughty <32392044+leedoughty@users.noreply.github.com>
memchr <memchr@proton.me>
Max Romanowski <maxr777@proton.me>
Aldlss <ayaldlss@gmail.com>
Hanna Nilsén <hanni614@student.liu.se>
Elias Johansson Lara <elias.johanssonlara@gmail.com>
Toby Penner <tobypenner01@gmail.com>
Danilo Spillebeen <spillebeendanilo@gmail.com>
Matbe766 <matildabergstrom01@gmail.com>
Amanda Sternberg <mandis.sternberg@gmail.com>
arold0 <arold0@icloud.com>
nav1s <nav1s@proton.me>
Ranjit Odedra <ranjitodedra.dev@gmail.com>
Eltaurus <https://github.com/Eltaurus-Lt>
jariji
Francisco Esteva <fr.esteva@duocuc.cl>
Junia Mannervik <junia.mannervik@gmail.com>
Emma Plante <emmaplante04@gmail.com>
SelfishPig <https://github.com/SelfishPig>
defkorean <https://github.com/defkorean>
Michael Lappas <https://github.com/michaellappas>
Brett Schwartz <brettschwartz871@gmail.com>
Lovro Boban <lovro.boban@hotmail.com>
Yuuki Gabriele Patriarca <yuukigpatriarca@gmail.com>
SecretX <https://github.com/SecretX33>
Daniel Pechersky <danny.pechersky@gmail.com>
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 <https://help.ankiweb.net>"]
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<Platform> {
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<Platform> {
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::<u32>().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::<u32>().unwrap_or(0).to_string();
format!("{normalized_prefix}{rest}")
}
}
})
.collect::<Vec<_>>()
.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<Platform>,
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::<Self>()
.split("::")
.last()
.unwrap()
.split('<')
.next()
.unwrap()
}
}
#[cfg(test)]
trait TestBuildAction {}
#[cfg(test)]
impl<T: TestBuildAction + ?Sized> 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>(T);
impl<T> TestBuildAction for WithGeneric<T> {}
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<I> ExtractArchive<'_, I> {
fn extraction_folder(&self) -> Utf8PathBuf {
Utf8Path::new("$builddir")
.join("extracted")
.join(self.extraction_folder_name)
}
}
impl<I> BuildAction for ExtractArchive<'_, I>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
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<I>(
build: &mut Build,
group_name: &str,
archive: OnlineArchive,
file_manifest: HashMap<&'static str, I>,
) -> Result<()>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
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<NodeFile>,
}
#[derive(Debug)]
struct NodeFile {
filename: String,
url: String,
}
fn main() -> Result<(), Box<dyn Error>> {
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<NodeRelease, Box<dyn Error>> {
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<Value> = 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::<Vec<_>>();
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<String, Box<dyn Error>> {
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<dyn Error>> {
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<String, Box<dyn Error>> {
// 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<dyn Error>> {
// 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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<String, Box<dyn Error>> {
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: "<sha256> <filename>"
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<String, Box<dyn Error>> {
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<String, Box<dyn Error>> {
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<dyn Error>> {
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<dyn Error>> {
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<String, Vec<String>>,
}
impl Build {
pub fn new() -> Result<Self> {
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<String>) {
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<str>, action: impl BuildAction) -> Result<()> {
let group = group.as_ref();
let groups = split_groups(group);
let group = groups[0];
let command = action.command();
let action_name = action.name();
// first invocation?
let mut first_invocation = false;
self.once_only(action_name, |build| {
action.on_first_instance(build)?;
first_invocation = true;
Ok(())
})?;
let action_name = action_name.to_string();
// ensure separator is delivered to runner, not shell
let command = if cfg!(windows) || action.bypass_runner() {
command.into()
} else {
command.replace("&&", "\"&&\"")
};
let mut statement = BuildStatement::from_build_action(
group,
action,
&self.groups,
self.build_profile,
self.have_n2,
);
if first_invocation {
let command = statement.prepare_command(command)?;
writeln!(
&mut self.output_text,
"\
rule {action_name}
command = {command}",
)
.unwrap();
for (k, v) in &statement.rule_variables {
writeln!(&mut self.output_text, " {k} = {v}").unwrap();
}
self.output_text.push('\n');
}
let (all_outputs, subgroups) = statement.render_into(&mut self.output_text);
for group in groups {
self.add_resolved_files_to_group(group, &all_outputs);
}
for (subgroup, outputs) in subgroups {
let group_with_subgroup = format!("{group}:{subgroup}");
self.add_resolved_files_to_group(&group_with_subgroup, &outputs);
}
Ok(())
}
/// Add one or more resolved files to a group. Does not add to the parent
/// groups; that must be done by the caller.
fn add_resolved_files_to_group<'a>(
&mut self,
group: &str,
files: impl IntoIterator<Item = &'a String>,
) {
let buf = self.groups.entry(group.to_owned()).or_default();
buf.extend(files.into_iter().map(ToString::to_string));
}
/// Allows you to add dependencies on files or build steps that aren't
/// required to build the group itself, but are required by consumers of
/// that group. Can also be used to allow substitution of local binaries
/// for downloaded ones (eg :node_binary).
pub fn add_dependency(&mut self, group: &str, deps: BuildInput) {
let files = self.expand_inputs(deps);
let groups = split_groups(group);
for group in groups {
self.add_resolved_files_to_group(group, &files);
}
}
/// Outputs from a given build statement group. An error if no files have
/// been registered yet.
pub fn group_outputs(&self, group_name: &'static str) -> &[String] {
self.groups
.get(group_name)
.unwrap_or_else(|| panic!("expected files in {group_name}"))
}
/// Single output from a given build statement group. An error if no files
/// have been registered yet, or more than one file has been registered.
pub fn group_output(&self, group_name: &'static str) -> String {
let outputs = self.group_outputs(group_name);
assert_eq!(outputs.len(), 1);
outputs.first().unwrap().into()
}
pub fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String> {
expand_inputs(inputs, &self.groups)
}
/// Expand inputs, the return a filtered subset.
pub fn filter_inputs<F>(&self, inputs: impl AsRef<BuildInput>, func: F) -> Vec<String>
where
F: FnMut(&String) -> bool,
{
self.expand_inputs(inputs)
.into_iter()
.filter(func)
.collect()
}
pub fn inputs_with_suffix(&self, inputs: impl AsRef<BuildInput>, ext: &str) -> Vec<String> {
self.filter_inputs(inputs, |f| f.ends_with(ext))
}
}
fn split_groups(group: &str) -> Vec<&str> {
let mut rest = group;
let mut groups = vec![group];
while let Some((head, _tail)) = rest.rsplit_once(':') {
groups.push(head);
rest = head;
}
groups
}
struct BuildStatement<'a> {
/// Cache of outputs by already-evaluated build rules, allowing later rules
/// to more easily consume the outputs of previous rules.
existing_outputs: &'a HashMap<String, Vec<String>>,
rule_name: &'static str,
// implicit refers to files that are not automatically assigned to $in and $out by Ninja,
implicit_inputs: Vec<String>,
implicit_outputs: Vec<String>,
explicit_inputs: Vec<String>,
explicit_outputs: Vec<String>,
order_only_inputs: Vec<String>,
output_subsets: Vec<(String, Vec<String>)>,
variables: Vec<(String, String)>,
rule_variables: Vec<(String, String)>,
output_stamp: bool,
env_vars: Vec<String>,
working_dir: Option<String>,
create_dirs: Vec<String>,
build_profile: BuildProfile,
bypass_runner: bool,
}
impl BuildStatement<'_> {
fn from_build_action<'a>(
group: &str,
mut action: impl BuildAction,
existing_outputs: &'a HashMap<String, Vec<String>>,
build_profile: BuildProfile,
have_n2: bool,
) -> BuildStatement<'a> {
let mut stmt = BuildStatement {
existing_outputs,
rule_name: action.name(),
implicit_inputs: Default::default(),
implicit_outputs: Default::default(),
explicit_inputs: Default::default(),
explicit_outputs: Default::default(),
order_only_inputs: Default::default(),
variables: Default::default(),
rule_variables: Default::default(),
output_subsets: Default::default(),
output_stamp: false,
env_vars: Default::default(),
working_dir: None,
create_dirs: Default::default(),
build_profile,
bypass_runner: action.bypass_runner(),
};
action.files(&mut stmt);
if stmt.explicit_outputs.is_empty() && stmt.implicit_outputs.is_empty() {
panic!("{} must generate at least one output", action.name());
}
stmt.variables.push(("description".into(), group.into()));
if action.check_output_timestamps() {
stmt.rule_variables.push(("restat".into(), "1".into()));
}
if action.generator() {
stmt.rule_variables.push(("generator".into(), "1".into()));
}
if let Some(pool) = action.concurrency_pool() {
stmt.rule_variables.push(("pool".into(), pool.into()));
}
if have_n2 {
if action.hide_success() {
stmt.rule_variables
.push(("hide_success".into(), "1".into()));
}
if action.hide_progress() {
stmt.rule_variables
.push(("hide_progress".into(), "1".into()));
}
}
stmt
}
/// Returns a list of all output files, which `Build` will add to
/// `existing_outputs`, and any subgroups.
fn render_into(mut self, buf: &mut String) -> (Vec<String>, Vec<(String, Vec<String>)>) {
let action_name = self.rule_name;
self.implicit_inputs.sort();
self.implicit_outputs.sort();
let inputs_str = to_ninja_target_string(
&self.explicit_inputs,
&self.implicit_inputs,
&self.order_only_inputs,
);
let outputs_str =
to_ninja_target_string(&self.explicit_outputs, &self.implicit_outputs, &[]);
writeln!(buf, "build {outputs_str}: {action_name} {inputs_str}").unwrap();
for (key, value) in self.variables.iter().sorted() {
writeln!(buf, " {key} = {value}").unwrap();
}
writeln!(buf).unwrap();
let outputs_vec = {
self.implicit_outputs.extend(self.explicit_outputs);
self.implicit_outputs
};
(outputs_vec, self.output_subsets)
}
fn prepare_command(&mut self, command: String) -> Result<String> {
if self.bypass_runner {
return Ok(command);
}
if command.starts_with("$runner") {
self.implicit_inputs.push("$runner".into());
return Ok(command);
}
let mut buf = String::from("$runner run ");
if self.output_stamp {
write!(&mut buf, "--stamp=$stamp ")?;
}
for var in &self.env_vars {
write!(&mut buf, "--env=\"{var}\" ")?;
}
for dir in &self.create_dirs {
write!(&mut buf, "--mkdir={dir} ")?;
}
if let Some(working_dir) = &self.working_dir {
write!(&mut buf, "--cwd={working_dir} ")?;
}
buf.push_str(&command);
Ok(buf)
}
}
fn expand_inputs(
input: impl AsRef<BuildInput>,
existing_outputs: &HashMap<String, Vec<String>>,
) -> Vec<String> {
let mut vec = vec![];
input.as_ref().add_to_vec(&mut vec, existing_outputs);
vec
}
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum BuildProfile {
Debug,
Release,
ReleaseWithLto,
}
impl BuildProfile {
fn from_env() -> Self {
match std::env::var("RELEASE").unwrap_or_default().as_str() {
"1" => Self::Release,
"2" => Self::ReleaseWithLto,
_ => Self::Debug,
}
}
}
pub trait FilesHandle {
/// Add inputs to the build statement. Can be called multiple times with
/// different variables. This is a shortcut for calling .expand_inputs()
/// and then .add_inputs_vec()
/// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By
/// convention, this is often `in`.
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>);
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>);
/// Add a variable that can be referenced in the command.
fn add_variable(&mut self, name: impl Into<String>, value: impl Into<String>);
fn expand_input(&self, input: &BuildInput) -> String;
fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String>;
/// Like [FilesHandle::add_outputs_ext], without adding a subgroup.
fn add_outputs(
&mut self,
variable: &'static str,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
) {
self.add_outputs_ext(variable, outputs, false);
}
/// Add outputs to the build statement. Can be called multiple times with
/// different variables.
/// - Each output automatically has $builddir/ prefixed to it if it does not
/// already start with it.
/// - If the variable name is non-empty, a variable of the same name will be
/// created so the file list can be accessed in the command. By
/// convention, this is often `out`.
/// - If subgroup is true, the files are also placed in a subgroup. Eg if a
/// rule `foo` exists and subgroup `bar` is provided, the files are
/// accessible via `:foo:bar`. The variable name must not be empty, or
/// called `out`.
fn add_outputs_ext(
&mut self,
variable: impl Into<String>,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
subgroup: bool,
);
/// Save an output stamp if the command completes successfully. Note that
/// if you have bypassed the runner, you will need to create the file
/// yourself.
fn add_output_stamp(&mut self, path: impl Into<String>);
/// Set an env var for the duration of the provided command(s).
/// Note this is defined once for the rule, so if the value should change
/// for each command, `constant_value` should reference a `$variable` you
/// have defined.
fn add_env_var(&mut self, key: &str, constant_value: &str);
/// Set the current working dir for the provided command(s).
/// Note this is defined once for the rule, so if the value should change
/// for each command, `constant_value` should reference a `$variable` you
/// have defined.
fn set_working_dir(&mut self, constant_value: &str);
/// Ensure provided folder and parent folders are created before running
/// the command. Can be called multiple times. Defines a variable pointing
/// at the folder.
fn create_dir_all(&mut self, key: &str, path: impl Into<String>);
fn build_profile(&self) -> BuildProfile;
}
impl FilesHandle for BuildStatement<'_> {
fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>) {
self.add_inputs_vec(variable, FilesHandle::expand_inputs(self, inputs));
}
fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<String>) {
match variable {
"in" => self.explicit_inputs.extend(inputs),
other_key => {
if !other_key.is_empty() {
self.add_variable(other_key, space_separated(&inputs));
}
self.implicit_inputs.extend(inputs);
}
}
}
fn add_order_only_inputs(&mut self, variable: &'static str, inputs: impl AsRef<BuildInput>) {
let inputs = FilesHandle::expand_inputs(self, inputs);
if !variable.is_empty() {
self.add_variable(variable, space_separated(&inputs))
}
self.order_only_inputs.extend(inputs);
}
fn add_variable(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.variables.push((key.into(), value.into()));
}
fn expand_input(&self, input: &BuildInput) -> String {
let mut vec = Vec::with_capacity(1);
input.add_to_vec(&mut vec, self.existing_outputs);
if vec.len() != 1 {
panic!("expected {input:?} to resolve to a single file; got ${vec:?}");
}
vec.pop().unwrap()
}
fn add_outputs_ext(
&mut self,
variable: impl Into<String>,
outputs: impl IntoIterator<Item = impl AsRef<str>>,
subgroup: bool,
) {
let outputs = outputs.into_iter().map(|v| {
let v = v.as_ref();
let v = if !v.starts_with("$builddir/") && !v.starts_with("$builddir\\") {
format!("$builddir/{v}")
} else {
v.to_owned()
};
if cfg!(windows) {
v.replace('/', "\\")
} else {
v
}
});
let variable = variable.into();
match variable.as_str() {
"out" => self.explicit_outputs.extend(outputs),
other_key => {
let outputs: Vec<_> = outputs.collect();
if !other_key.is_empty() {
self.add_variable(other_key, space_separated(&outputs));
}
if subgroup {
assert!(!other_key.is_empty());
self.output_subsets
.push((other_key.to_owned(), outputs.to_owned()));
}
self.implicit_outputs.extend(outputs);
}
}
}
fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String> {
expand_inputs(inputs, self.existing_outputs)
}
fn build_profile(&self) -> BuildProfile {
self.build_profile
}
fn add_output_stamp(&mut self, path: impl Into<String>) {
self.output_stamp = true;
self.add_outputs("stamp", vec![path.into()]);
}
fn add_env_var(&mut self, key: &str, constant_value: &str) {
self.env_vars.push(format!("{key}={constant_value}"));
}
fn set_working_dir(&mut self, constant_value: &str) {
self.working_dir = Some(constant_value.to_owned());
}
fn create_dir_all(&mut self, key: &str, path: impl Into<String>) {
let path = path.into();
self.add_variable(key, &path);
self.create_dirs.push(path);
}
}
fn to_ninja_target_string(
explicit: &[String],
implicit: &[String],
order_only: &[String],
) -> String {
let mut joined = space_separated(explicit);
if !implicit.is_empty() {
joined.push_str(" | ");
joined.push_str(&space_separated(implicit));
}
if !order_only.is_empty() {
joined.push_str(" || ");
joined.push_str(&space_separated(order_only));
}
joined
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_split_groups() {
assert_eq!(&split_groups("foo"), &["foo"]);
assert_eq!(&split_groups("foo:bar"), &["foo:bar", "foo"]);
assert_eq!(
&split_groups("foo:bar:baz"),
&["foo:bar:baz", "foo:bar", "foo"]
);
}
}
================================================
FILE: build/ninja_gen/src/cargo.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 camino::Utf8Path;
use camino::Utf8PathBuf;
use crate::action::BuildAction;
use crate::archives::with_exe;
use crate::build::BuildProfile;
use crate::build::FilesHandle;
use crate::input::BuildInput;
use crate::inputs;
use crate::Build;
#[derive(Debug, PartialEq, Eq)]
pub enum RustOutput<'a> {
Binary(&'a str),
StaticLib(&'a str),
DynamicLib(&'a str),
/// (group_name, fully qualified path)
Data(&'a str, &'a str),
}
impl RustOutput<'_> {
pub fn name(&self) -> &str {
match self {
RustOutput::Binary(pkg) => pkg,
RustOutput::StaticLib(pkg) => pkg,
RustOutput::DynamicLib(pkg) => pkg,
RustOutput::Data(name, _) => name,
}
}
pub fn path(
&self,
rust_base: &Utf8Path,
target: Option<&str>,
build_profile: BuildProfile,
) -> String {
let filename = match *self {
RustOutput::Binary(package) => {
if cfg!(windows) {
format!("{package}.exe")
} else {
package.into()
}
}
RustOutput::StaticLib(package) => format!("lib{package}.a"),
RustOutput::DynamicLib(package) => {
if cfg!(windows) {
format!("{package}.dll")
} else if cfg!(target_os = "macos") {
format!("lib{package}.dylib")
} else {
format!("lib{package}.so")
}
}
RustOutput::Data(_, path) => return path.to_string(),
};
let mut path: Utf8PathBuf = rust_base.into();
if let Some(target) = target {
path = path.join(target);
}
path = path.join(profile_output_dir(build_profile)).join(filename);
path.to_string()
}
}
fn profile_output_dir(profile: BuildProfile) -> &'static str {
match profile {
BuildProfile::Debug => "debug",
BuildProfile::Release => "release",
BuildProfile::ReleaseWithLto => "release-lto",
}
}
#[derive(Debug, Default)]
pub struct CargoBuild<'a> {
pub inputs: BuildInput,
pub outputs: &'a [RustOutput<'a>],
pub target: Option<&'static str>,
pub extra_args: &'a str,
pub release_override: Option<BuildProfile>,
}
impl BuildAction for CargoBuild<'_> {
fn command(&self) -> &str {
"cargo build $release_arg $target_arg $cargo_flags $extra_args"
}
fn files(&mut self, build: &mut impl FilesHandle) {
let release_build = self
.release_override
.unwrap_or_else(|| build.build_profile());
let release_arg = profile_arg_for_cargo(release_build).unwrap_or_default();
let target_arg = if let Some(target) = self.target {
format!("--target {target}")
} else {
"".into()
};
build.add_inputs("", &self.inputs);
build.add_inputs(
"",
inputs![".cargo/config.toml", "rust-toolchain.toml", "Cargo.lock"],
);
build.add_variable("release_arg", release_arg);
build.add_variable("target_arg", target_arg);
build.add_variable("extra_args", self.extra_args);
let output_root = Utf8Path::new("$builddir/rust");
for output in self.outputs {
let name = output.name();
let path = output.path(output_root, self.target, release_build);
build.add_outputs_ext(name, vec![path], true);
}
}
fn check_output_timestamps(&self) -> bool {
true
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
setup_flags(build)
}
}
fn profile_arg_for_cargo(profile: BuildProfile) -> Option<&'static str> {
match profile {
BuildProfile::Debug => None,
BuildProfile::Release => Some("--release"),
BuildProfile::ReleaseWithLto => Some("--profile release-lto"),
}
}
fn setup_flags(build: &mut Build) -> Result<()> {
build.once_only("cargo_flags_and_pool", |build| {
build.variable("cargo_flags", "--locked");
Ok(())
})
}
pub struct CargoTest {
pub inputs: BuildInput,
}
impl BuildAction for CargoTest {
fn command(&self) -> &str {
"cargo nextest run --color=always --failure-output=final --status-level=none $cargo_flags"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("", &self.inputs);
build.add_inputs("", inputs![":cargo-nextest"]);
build.add_env_var("ANKI_TEST_MODE", "1");
build.add_output_stamp("tests/cargo_test");
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
build.add_action(
"cargo-nextest",
CargoInstall {
binary_name: "cargo-nextest",
args: "cargo-nextest --version 0.9.99 --locked --no-default-features --features default-no-update",
},
)?;
setup_flags(build)
}
}
pub struct CargoClippy {
pub inputs: BuildInput,
}
impl BuildAction for CargoClippy {
fn command(&self) -> &str {
"cargo clippy $cargo_flags --tests -- -Dclippy::dbg_macro -Dwarnings"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs(
"",
inputs![&self.inputs, "Cargo.lock", "rust-toolchain.toml"],
);
build.add_output_stamp("tests/cargo_clippy");
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
setup_flags(build)
}
}
pub struct CargoFormat {
pub inputs: BuildInput,
pub check_only: bool,
pub working_dir: Option<&'static str>,
}
impl BuildAction for CargoFormat {
fn command(&self) -> &str {
"cargo fmt $mode --all"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("", &self.inputs);
build.add_variable("mode", if self.check_only { "--check" } else { "" });
if let Some(working_dir) = self.working_dir {
build.set_working_dir("$working_dir");
build.add_variable("working_dir", working_dir);
}
build.add_output_stamp(format!(
"tests/cargo_format.{}",
if self.check_only { "check" } else { "fmt" }
));
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
setup_flags(build)
}
}
/// Use Cargo to download and build a Rust binary. If `binary_name` is `foo`, a
/// `$foo` variable will be defined with the path to the binary.
pub struct CargoInstall {
pub binary_name: &'static str,
/// eg 'foo --version 1.3' or '--git git://...'
pub args: &'static str,
}
impl BuildAction for CargoInstall {
fn command(&self) -> &str {
"cargo install --color always $args --root $builddir"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_variable("args", self.args);
build.add_outputs("", vec![with_exe(&format!("bin/{}", self.binary_name))])
}
fn check_output_timestamps(&self) -> bool {
true
}
}
pub struct CargoRun {
pub binary_name: &'static str,
pub cargo_args: &'static str,
pub bin_args: &'static str,
pub deps: BuildInput,
}
impl BuildAction for CargoRun {
fn command(&self) -> &str {
"cargo run --bin $binary $cargo_args -- $bin_args"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("", &self.deps);
build.add_variable("binary", self.binary_name);
build.add_variable("cargo_args", self.cargo_args);
build.add_variable("bin_args", self.bin_args);
build.add_outputs("", vec![format!("phony-{}", self.binary_name)]);
}
}
================================================
FILE: build/ninja_gen/src/command.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 crate::action::BuildAction;
use crate::input::space_separated;
use crate::input::BuildInput;
use crate::inputs;
pub struct RunCommand<'a> {
// Will be automatically included as a dependency
pub command: &'static str,
// Arguments to the script, eg `$in $out` or `$in > $out`.
pub args: &'a str,
pub inputs: HashMap<&'static str, BuildInput>,
pub outputs: HashMap<&'static str, Vec<&'a str>>,
}
impl BuildAction for RunCommand<'_> {
fn command(&self) -> &str {
"$cmd $args"
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
// Because we've defined a generic rule instead of making one for a specific use
// case, we need to manually intepolate variables in the user-provided
// args.
let mut args = self.args.to_string();
for (key, inputs) in &self.inputs {
let files = build.expand_inputs(inputs);
build.add_inputs("", inputs);
if !key.is_empty() {
args = args.replace(&format!("${key}"), &space_separated(files));
}
}
for (key, outputs) in &self.outputs {
if !key.is_empty() {
let outputs = outputs.iter().map(|o| {
if !o.starts_with("$builddir/") {
format!("$builddir/{o}")
} else {
(*o).into()
}
});
args = args.replace(&format!("${key}"), &space_separated(outputs));
}
}
build.add_inputs("cmd", inputs![self.command]);
build.add_variable("args", args);
for outputs in self.outputs.values() {
build.add_outputs("", outputs);
}
}
}
================================================
FILE: build/ninja_gen/src/configure.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::action::BuildAction;
use crate::build::BuildProfile;
use crate::build::FilesHandle;
use crate::cargo::CargoBuild;
use crate::cargo::RustOutput;
use crate::glob;
use crate::inputs;
use crate::Build;
pub struct ConfigureBuild {}
impl BuildAction for ConfigureBuild {
fn command(&self) -> &str {
"$cmd"
}
fn files(&mut self, build: &mut impl FilesHandle) {
build.add_inputs("cmd", inputs![":build:configure_bin"]);
// reconfigure when external inputs change
build.add_inputs("", inputs!["$builddir/env", ".version", ".git"]);
build.add_outputs("", ["build.ninja"])
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
build.add_action(
"build:configure_bin",
CargoBuild {
inputs: inputs![glob!["build/**/*"]],
outputs: &[RustOutput::Binary("configure")],
target: None,
extra_args: "-p configure",
release_override: Some(BuildProfile::Debug),
},
)?;
Ok(())
}
fn generator(&self) -> bool {
true
}
fn check_output_timestamps(&self) -> bool {
true
}
}
================================================
FILE: build/ninja_gen/src/copy.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use camino::Utf8Path;
use crate::action::BuildAction;
use crate::input::BuildInput;
/// Copy the provided files into the specified destination folder.
/// Directory structure is not preserved - eg foo/bar.js is copied
/// into out/$output_folder/bar.js.
pub struct CopyFiles<'a> {
pub inputs: BuildInput,
/// The folder (relative to the build folder) that files should be copied
/// into.
pub output_folder: &'a str,
}
impl BuildAction for CopyFiles<'_> {
fn command(&self) -> &str {
// The -f is because we may need to overwrite read-only files copied from Bazel.
"cp -fr $in $builddir/$folder"
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
let inputs = build.expand_inputs(&self.inputs);
let output_folder = Utf8Path::new(self.output_folder);
let outputs: Vec<_> = inputs
.iter()
.map(|f| output_folder.join(Utf8Path::new(f).file_name().unwrap()))
.collect();
build.add_inputs("in", &self.inputs);
build.add_outputs("", outputs);
build.add_variable("folder", self.output_folder);
}
}
/// Copy a single file to the provided output path, which should be relative to
/// the output folder. This can be used to create a copy with a different name.
pub struct CopyFile<'a> {
pub input: BuildInput,
pub output: &'a str,
}
impl BuildAction for CopyFile<'_> {
fn command(&self) -> &str {
"cp $in $out"
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
build.add_inputs("in", &self.input);
build.add_outputs("out", vec![self.output]);
}
}
/// Create a symbolic link to the provided output path, which should be relative
/// to the output folder. This can be used to create a copy with a different
/// name.
pub struct LinkFile<'a> {
pub input: BuildInput,
pub output: &'a str,
}
impl BuildAction for LinkFile<'_> {
fn command(&self) -> &str {
if cfg!(windows) {
"cmd /c copy $in $out"
} else {
"ln -sf $in $out"
}
}
fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
build.add_inputs("in", &self.input);
build.add_outputs("out", vec![self.output]);
}
fn check_output_timestamps(&self) -> bool {
true
}
}
================================================
FILE: build/ninja_gen/src/git.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 itertools::Itertools;
use super::*;
use crate::action::BuildAction;
use crate::input::BuildInput;
pub struct SyncSubmodule {
pub path: &'static str,
pub offline_build: bool,
}
impl BuildAction for SyncSubmodule {
fn command(&self) -> &str {
if self.offline_build {
"echo OFFLINE_BUILD is set, skipping git repository update for $path"
} else {
"git -c protocol.file.allow=always submodule update --checkout --init $path"
}
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
if !self.offline_build {
if let Some(head) = locate_git_head() {
build.add_inputs("", head);
} else {
println!("Warning, .git/HEAD not found; submodules may be stale");
}
}
build.add_variable("path", self.path);
build.add_output_stamp(format!("git/{}", self.path));
}
fn on_first_instance(&self, build: &mut Build) -> Result<()> {
build.pool("git", 1);
Ok(())
}
fn concurrency_pool(&self) -> Option<&'static str> {
Some("git")
}
}
/// We check the mtime of .git/HEAD to detect when we should sync submodules.
/// If this repo is a submodule of another project, .git/HEAD will not exist,
/// and we fall back on .git/modules/*/HEAD in a parent folder instead.
fn locate_git_head() -> Option<BuildInput> {
let standard_path = Utf8Path::new(".git/HEAD");
if standard_path.exists() {
return Some(inputs![standard_path.to_string()]);
}
let mut folder = Utf8PathBuf::from_path_buf(
dunce::canonicalize(Utf8Path::new(".").canonicalize().unwrap()).unwrap(),
)
.unwrap();
loop {
let path = folder.join(".git").join("modules");
if path.exists() {
let heads = path
.read_dir_utf8()
.unwrap()
.filter_map(|p| {
let head = p.unwrap().path().join("HEAD");
if head.exists() {
Some(head.as_str().replace(':', "$:"))
} else {
None
}
})
.collect_vec();
return Some(inputs![heads]);
}
if let Some(parent) = folder.parent() {
folder = parent.to_owned();
} else {
return None;
}
}
}
================================================
FILE: build/ninja_gen/src/hash.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
pub fn simple_hash(hashable: impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
hashable.hash(&mut hasher);
hasher.finish()
}
================================================
FILE: build/ninja_gen/src/input.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::fmt::Display;
use std::sync::LazyLock;
use camino::Utf8PathBuf;
#[derive(Debug, Clone, Hash, Default)]
pub enum BuildInput {
Single(String),
Multiple(Vec<String>),
Glob(Glob),
Inputs(Vec<BuildInput>),
#[default]
Empty,
}
impl AsRef<BuildInput> for BuildInput {
fn as_ref(&self) -> &BuildInput {
self
}
}
impl From<String> for BuildInput {
fn from(v: String) -> Self {
BuildInput::Single(v)
}
}
impl From<&str> for BuildInput {
fn from(v: &str) -> Self {
BuildInput::Single(v.to_owned())
}
}
impl From<Vec<String>> for BuildInput {
fn from(v: Vec<String>) -> Self {
BuildInput::Multiple(v)
}
}
impl From<Glob> for BuildInput {
fn from(v: Glob) -> Self {
BuildInput::Glob(v)
}
}
impl From<&BuildInput> for BuildInput {
fn from(v: &BuildInput) -> Self {
BuildInput::Inputs(vec![v.clone()])
}
}
impl From<&[BuildInput]> for BuildInput {
fn from(v: &[BuildInput]) -> Self {
BuildInput::Inputs(v.to_vec())
}
}
impl From<Vec<BuildInput>> for BuildInput {
fn from(v: Vec<BuildInput>) -> Self {
BuildInput::Inputs(v)
}
}
impl From<Utf8PathBuf> for BuildInput {
fn from(v: Utf8PathBuf) -> Self {
BuildInput::Single(v.into_string())
}
}
impl BuildInput {
pub fn add_to_vec(
&self,
vec: &mut Vec<String>,
exisiting_outputs: &HashMap<String, Vec<String>>,
) {
let mut resolve_and_add = |value: &str| {
if let Some(stripped) = value.strip_prefix(':') {
let files = exisiting_outputs.get(stripped).unwrap_or_else(|| {
println!("{:?}", &exisiting_outputs);
panic!("input referenced {value}, but rule missing/not processed");
});
for file in files {
vec.push(file.into())
}
} else {
vec.push(value.into());
}
};
match self {
BuildInput::Single(s) => resolve_and_add(s),
BuildInput::Multiple(v) => {
for item in v {
resolve_and_add(item);
}
}
BuildInput::Glob(glob) => {
for path in glob.resolve() {
vec.push(path.into_string());
}
}
BuildInput::Inputs(inputs) => {
for input in inputs {
input.add_to_vec(vec, exisiting_outputs)
}
}
BuildInput::Empty => {}
}
}
}
#[derive(Debug, Clone, Hash)]
pub struct Glob {
pub include: String,
pub exclude: Option<String>,
}
static CACHED_FILES: LazyLock<Vec<Utf8PathBuf>> = LazyLock::new(cache_files);
/// Walking the source tree once instead of for each glob yields ~4x speed
/// improvements.
fn cache_files() -> Vec<Utf8PathBuf> {
walkdir::WalkDir::new(".")
// ensure the output order is predictable
.sort_by_file_name()
.into_iter()
.filter_entry(move |e| {
// don't walk into symlinks, or the top-level out/, or .git
!(e.path_is_symlink()
|| (e.depth() == 1 && (e.file_name() == "out" || e.file_name() == ".git")))
})
.filter_map(move |e| {
let path = e.as_ref().unwrap().path().strip_prefix("./").unwrap();
if !path.is_dir() {
Some(Utf8PathBuf::from_path_buf(path.to_owned()).unwrap())
} else {
None
}
})
.collect()
}
impl Glob {
pub fn resolve(&self) -> impl Iterator<Item = Utf8PathBuf> {
let include = globset::GlobBuilder::new(&self.include)
.literal_separator(true)
.build()
.unwrap()
.compile_matcher();
let exclude = self.exclude.as_ref().map(|glob| {
globset::GlobBuilder::new(glob)
.literal_separator(true)
.build()
.unwrap()
.compile_matcher()
});
CACHED_FILES.iter().filter_map(move |path| {
if include.is_match(path) {
let excluded = exclude
.as_ref()
.map(|exclude| exclude.is_match(path))
.unwrap_or_default();
if !excluded {
return Some(path.to_owned());
}
}
None
})
}
}
pub fn space_separated<I>(iter: I) -> String
where
I: IntoIterator,
I::Item: Display,
{
itertools::join(iter, " ")
}
================================================
FILE: build/ninja_gen/src/lib.rs
================================================
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub mod action;
pub mod archives;
pub mod build;
pub mod cargo;
pub mod command;
pub mod configure;
pub mod copy;
pub mod git;
pub mod hash;
pub mod input;
pub mod node;
pub mod protobuf;
pub mod python;
pub mod render;
pub mod rsync;
pub mod sass;
pub use build::Build;
pub use camino::Utf8Path;
pub use camino::Utf8PathBuf;
pub use maplit::hashmap;
pub use which::which;
#[macro_export]
macro_rules! inputs {
($($param:expr),+ $(,)?) => {
$crate::input::BuildInput::from(vec![$($crate::input::BuildInput::from($param)),+])
};
() => {
$crate::input::BuildInput::Empty
};
}
#[macro_export]
macro_rules! glob {
($include:expr) => {
$crate::input::Glob {
include: $include.into(),
exclude: None,
}
};
($include:expr, $exclude:expr) => {
$crate::input::Glob {
include: $include.into(),
exclude: Some($exclude.into()),
}
};
}
================================================
FILE: build/ninja_gen/src/node.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 itertools::Itertools;
use super::*;
use crate::action::BuildAction;
use crate::archives::download_and_extract;
use crate::archives::OnlineArchive;
use crate::archives::Platform;
use crate::hash::simple_hash;
use crate::input::space_separated;
use crate::input::BuildInput;
pub fn node_archive(platform: Platform) -> OnlineArchive {
match platform {
Platform::LinuxX64 => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz",
sha256: "325c0f1261e0c61bcae369a1274028e9cfb7ab7949c05512c5b1e630f7e80e12",
},
Platform::LinuxArm => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-arm64.tar.xz",
sha256: "140aee84be6774f5fb3f404be72adbe8420b523f824de82daeb5ab218dab7b18",
},
Platform::MacX64 => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-darwin-x64.tar.xz",
sha256: "f79de1f64df4ac68493a344bb5ab7d289d0275271e87b543d1278392c9de778a",
},
Platform::MacArm => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-darwin-arm64.tar.xz",
sha256: "cc9cc294eaf782dd93c8c51f460da610cc35753c6a9947411731524d16e97914",
},
Platform::WindowsX64 => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-win-x64.zip",
sha256: "721ab118a3aac8584348b132767eadf51379e0616f0db802cc1e66d7f0d98f85",
},
Platform::WindowsArm => OnlineArchive {
url: "https://nodejs.org/dist/v22.17.0/node-v22.17.0-win-arm64.zip",
sha256: "78355dc9ca117bb71d3f081e4b1b281855e2b134f3939bb0ca314f7567b0e621",
},
}
}
pub struct YarnSetup {}
impl BuildAction for YarnSetup {
fn command(&self) -> &str {
if cfg!(windows) {
"corepack.cmd enable yarn"
} else {
"corepack enable yarn"
}
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("", inputs![":node_binary"]);
build.add_outputs_ext(
"bin",
vec![if cfg!(windows) {
"extracted/node/yarn.cmd"
} else {
"extracted/node/bin/yarn"
}],
true,
);
}
fn check_output_timestamps(&self) -> bool {
true
}
}
pub struct YarnInstall<'a> {
pub package_json_and_lock: BuildInput,
pub exports: HashMap<&'a str, Vec<Cow<'a, str>>>,
}
impl BuildAction for YarnInstall<'_> {
fn command(&self) -> &str {
"$runner yarn $yarn $out"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("", &self.package_json_and_lock);
build.add_inputs("yarn", inputs![":yarn:bin"]);
build.add_outputs("out", vec!["node_modules/.marker"]);
for (key, value) in &self.exports {
let outputs: Vec<_> = value.iter().map(|o| format!("node_modules/{o}")).collect();
build.add_outputs_ext(*key, outputs, true);
}
}
fn check_output_timestamps(&self) -> bool {
true
}
}
fn with_cmd_ext(bin: &str) -> Cow<'_, str> {
if cfg!(windows) {
format!("{bin}.cmd").into()
} else {
bin.into()
}
}
pub fn setup_node(
build: &mut Build,
archive: OnlineArchive,
binary_exports: &[&'static str],
mut data_exports: HashMap<&str, Vec<Cow<str>>>,
) -> Result<()> {
let node_binary = match std::env::var("NODE_BINARY") {
Ok(path) => {
assert!(
Utf8Path::new(&path).is_absolute(),
"NODE_BINARY must be absolute"
);
path.into()
}
Err(_) => {
download_and_extract(
build,
"node",
archive,
hashmap! {
"bin" => vec![if cfg!(windows) { "node.exe" } else { "bin/node" }],
"npm" => vec![if cfg!(windows) { "npm.cmd " } else { "bin/npm" }]
},
)?;
inputs![":extract:node:bin"]
}
};
build.add_dependency("node_binary", node_binary);
match std::env::var("YARN_BINARY") {
Ok(path) => {
assert!(
Utf8Path::new(&path).is_absolute(),
"YARN_BINARY must be absolute"
);
build.add_dependency("yarn:bin", inputs![path]);
}
Err(_) => {
build.add_action("yarn", YarnSetup {})?;
}
};
for binary in binary_exports {
data_exports.insert(
*binary,
vec![format!(".bin/{}", with_cmd_ext(binary)).into()],
);
}
build.add_action(
"node_modules",
YarnInstall {
package_json_and_lock: inputs!["yarn.lock", "package.json"],
exports: data_exports,
},
)?;
Ok(())
}
pub struct EsbuildScript<'a> {
pub script: BuildInput,
pub entrypoint: BuildInput,
pub deps: BuildInput,
/// .js will be appended, and any extra extensions
pub output_stem: &'a str,
/// eg ['.css', '.html']
pub extra_exts: &'a [&'a str],
}
impl BuildAction for EsbuildScript<'_> {
fn command(&self) -> &str {
"$node_bin $script $entrypoint $out"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("node_bin", inputs![":node_binary"]);
build.add_inputs("script", &self.script);
build.add_inputs("entrypoint", &self.entrypoint);
build.add_inputs("", inputs!["yarn.lock", ":node_modules", &self.deps]);
build.add_inputs("", inputs!["out/env"]);
let stem = self.output_stem;
let mut outs = vec![format!("{stem}.js")];
outs.extend(self.extra_exts.iter().map(|ext| format!("{stem}.{ext}")));
build.add_outputs("out", outs);
}
}
pub struct DPrint {
pub inputs: BuildInput,
pub check_only: bool,
}
impl BuildAction for DPrint {
fn command(&self) -> &str {
"$dprint $mode"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("dprint", inputs![":node_modules:dprint"]);
build.add_inputs("", &self.inputs);
let mode = if self.check_only { "check" } else { "fmt" };
build.add_variable("mode", mode);
build.add_output_stamp(format!("tests/dprint.{mode}"));
}
}
pub struct Prettier {
pub inputs: BuildInput,
pub check_only: bool,
}
impl BuildAction for Prettier {
fn command(&self) -> &str {
"$yarn prettier --cache $mode $pattern"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("yarn", inputs![":yarn:bin"]);
build.add_inputs("prettier", inputs![":node_modules:prettier"]);
build.add_inputs("", &self.inputs);
build.add_variable("pattern", r#""**/*.svelte""#);
let (file_ext, mode) = if self.check_only {
("fmt", "--check")
} else {
("check", "--write")
};
build.add_variable("mode", mode);
build.add_output_stamp(format!("tests/prettier.{file_ext}"));
}
}
pub struct SvelteCheck {
pub tsconfig: BuildInput,
pub inputs: BuildInput,
}
impl BuildAction for SvelteCheck {
fn command(&self) -> &str {
"$yarn svelte-check:once"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("svelte-check", inputs![":node_modules:svelte-check"]);
build.add_inputs("tsconfig", &self.tsconfig);
build.add_inputs("yarn", inputs![":yarn:bin"]);
build.add_inputs("", &self.inputs);
build.add_inputs("", inputs!["yarn.lock"]);
let hash = simple_hash(&self.tsconfig);
build.add_output_stamp(format!("tests/svelte-check.{hash}"));
}
fn hide_progress(&self) -> bool {
true
}
}
pub struct TypescriptCheck {
pub tsconfig: BuildInput,
pub inputs: BuildInput,
}
impl BuildAction for TypescriptCheck {
fn command(&self) -> &str {
"$tsc --noEmit -p $tsconfig"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("tsc", inputs![":node_modules:tsc"]);
build.add_inputs("tsconfig", &self.tsconfig);
build.add_inputs("", &self.inputs);
build.add_inputs("", inputs!["yarn.lock"]);
let hash = simple_hash(&self.tsconfig);
build.add_output_stamp(format!("tests/typescript.{hash}"));
}
}
pub struct Eslint<'a> {
pub folder: &'a str,
pub inputs: BuildInput,
pub eslint_rc: BuildInput,
pub fix: bool,
}
impl BuildAction for Eslint<'_> {
fn command(&self) -> &str {
"$eslint --max-warnings=0 -c $eslint_rc $fix $folder"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("eslint", inputs![":node_modules:eslint"]);
build.add_inputs("eslint_rc", &self.eslint_rc);
build.add_inputs("in", &self.inputs);
build.add_inputs("", inputs!["yarn.lock", "ts/tsconfig.json"]);
build.add_variable("fix", if self.fix { "--fix" } else { "" });
build.add_variable("folder", self.folder);
let hash = simple_hash(self.folder);
let kind = if self.fix { "fix" } else { "check" };
build.add_output_stamp(format!("tests/eslint.{kind}.{hash}"));
}
}
pub struct ViteTest {
pub deps: BuildInput,
}
impl BuildAction for ViteTest {
fn command(&self) -> &str {
"$yarn vitest:once"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("vitest", inputs![":node_modules:vitest"]);
build.add_inputs("yarn", inputs![":yarn:bin"]);
build.add_inputs("", &self.deps);
build.add_output_stamp("tests/vitest");
}
}
pub struct SqlFormat {
pub inputs: BuildInput,
pub check_only: bool,
}
impl BuildAction for SqlFormat {
fn command(&self) -> &str {
"$tsx $sql_format $mode $in"
}
fn files(&mut self, build: &mut impl build::FilesHandle) {
build.add_inputs("tsx", inputs![":node_modules:tsx"]);
build.add_inputs("sql_format", inputs!["ts/tools/sql_format.ts"]);
build.add_inputs("in", &self.inputs);
let mode = if self.check_only { "check" } else { "fix" };
build.add_variable("mode", mode);
build.add_output_stamp(format!("tests/sql_format.{mode}"));
}
}
pub struct GenTypescriptProto<'a> {
pub protos: BuildInput,
pub i
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
Showing preview only (796K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (9270 symbols across 755 files)
FILE: build/configure/src/aqt.rs
function build_and_check_aqt (line 26) | pub fn build_and_check_aqt(build: &mut Build) -> Result<()> {
function build_forms (line 35) | fn build_forms(build: &mut Build) -> Result<()> {
function build_generated_sources (line 65) | fn build_generated_sources(build: &mut Build) -> Result<()> {
function build_data_folder (line 109) | fn build_data_folder(build: &mut Build) -> Result<()> {
function copy_sveltekit (line 119) | fn copy_sveltekit(build: &mut Build) -> Result<()> {
function build_css (line 131) | fn build_css(build: &mut Build) -> Result<()> {
function build_imgs (line 162) | fn build_imgs(build: &mut Build) -> Result<()> {
function build_js (line 172) | fn build_js(build: &mut Build) -> Result<()> {
function build_vendor_js (line 207) | fn build_vendor_js(build: &mut Build) -> Result<()> {
function build_pages (line 223) | fn build_pages(build: &mut Build) -> Result<()> {
function build_icons (line 234) | fn build_icons(build: &mut Build) -> Result<()> {
function build_themed_icons (line 271) | fn build_themed_icons(build: &mut Build) -> Result<()> {
type BuildThemedIcon (line 299) | struct BuildThemedIcon<'a> {
method command (line 305) | fn command(&self) -> &str {
method files (line 309) | fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
function build_wheel (line 338) | fn build_wheel(build: &mut Build) -> Result<()> {
function check_python (line 355) | fn check_python(build: &mut Build) -> Result<()> {
FILE: build/configure/src/launcher.rs
function setup_uv_universal (line 13) | pub fn setup_uv_universal(build: &mut Build) -> Result<()> {
function build_launcher (line 34) | pub fn build_launcher(build: &mut Build) -> Result<()> {
constant NSIS_PLUGINS (line 41) | const NSIS_PLUGINS: OnlineArchive = OnlineArchive {
FILE: build/configure/src/main.rs
function anki_version (line 36) | fn anki_version() -> String {
function main (line 43) | fn main() -> Result<()> {
FILE: build/configure/src/platform.rs
function overriden_rust_target_triple (line 9) | pub fn overriden_rust_target_triple() -> Option<&'static str> {
function overriden_python_venv_platform (line 15) | pub fn overriden_python_venv_platform() -> Option<Platform> {
function overriden_python_wheel_platform (line 26) | pub fn overriden_python_wheel_platform() -> Option<Platform> {
FILE: build/configure/src/pylib.rs
function build_pylib (line 21) | pub fn build_pylib(build: &mut Build) -> Result<()> {
function check_pylib (line 79) | pub fn check_pylib(build: &mut Build) -> Result<()> {
type GenBuildInfo (line 92) | pub struct GenBuildInfo {}
method command (line 95) | fn command(&self) -> &str {
method files (line 99) | fn files(&mut self, build: &mut impl ninja_gen::build::FilesHandle) {
FILE: build/configure/src/python.rs
function normalize_version (line 20) | fn normalize_version(version: &str) -> String {
function setup_venv (line 57) | pub fn setup_venv(build: &mut Build) -> Result<()> {
type GenPythonProto (line 77) | pub struct GenPythonProto {
method command (line 82) | fn command(&self) -> &str {
method files (line 90) | fn files(&mut self, build: &mut impl FilesHandle) {
method hide_progress (line 108) | fn hide_progress(&self) -> bool {
type BuildWheel (line 113) | pub struct BuildWheel {
method command (line 121) | fn command(&self) -> &str {
method files (line 125) | fn files(&mut self, build: &mut impl FilesHandle) {
function check_python (line 175) | pub fn check_python(build: &mut Build) -> Result<()> {
type Sphinx (line 224) | struct Sphinx {
method command (line 229) | fn command(&self) -> &str {
method files (line 237) | fn files(&mut self, build: &mut impl FilesHandle) {
method hide_success (line 253) | fn hide_success(&self) -> bool {
function setup_sphinx (line 258) | pub(crate) fn setup_sphinx(build: &mut Build) -> Result<()> {
function test_normalize_version_basic (line 285) | fn test_normalize_version_basic() {
function test_normalize_version_with_prerelease (line 292) | fn test_normalize_version_with_prerelease() {
FILE: build/configure/src/rust.rs
function build_rust (line 24) | pub fn build_rust(build: &mut Build) -> Result<()> {
function prepare_translations (line 30) | fn prepare_translations(build: &mut Build) -> Result<()> {
type FtlCommand (line 99) | struct FtlCommand {
method command (line 105) | fn command(&self) -> &str {
method files (line 109) | fn files(&mut self, build: &mut impl FilesHandle) {
function build_proto_descriptors_and_interfaces (line 117) | fn build_proto_descriptors_and_interfaces(build: &mut Build) -> Result<(...
function build_rsbridge (line 136) | fn build_rsbridge(build: &mut Build) -> Result<()> {
function check_rust (line 170) | pub fn check_rust(build: &mut Build) -> Result<()> {
function check_minilints (line 211) | pub fn check_minilints(build: &mut Build) -> Result<()> {
FILE: build/configure/src/web.rs
function build_and_check_web (line 25) | pub fn build_and_check_web(build: &mut Build) -> Result<()> {
function build_sveltekit (line 40) | fn build_sveltekit(build: &mut Build) -> Result<()> {
function setup_node (line 54) | fn setup_node(build: &mut Build) -> Result<()> {
function build_and_check_tslib (line 131) | fn build_and_check_tslib(build: &mut Build) -> Result<()> {
function declare_and_check_other_libraries (line 165) | fn declare_and_check_other_libraries(build: &mut Build) -> Result<()> {
function build_and_check_pages (line 182) | fn build_and_check_pages(build: &mut Build) -> Result<()> {
function build_and_check_editor (line 235) | fn build_and_check_editor(build: &mut Build) -> Result<()> {
function build_and_check_reviewer (line 262) | fn build_and_check_reviewer(build: &mut Build) -> Result<()> {
function check_web (line 310) | fn check_web(build: &mut Build) -> Result<()> {
function check_sql (line 391) | pub fn check_sql(build: &mut Build) -> Result<()> {
function build_and_check_mathjax (line 409) | fn build_and_check_mathjax(build: &mut Build) -> Result<()> {
constant MATHJAX_FILES (line 423) | pub const MATHJAX_FILES: &[&str] = &[
function copy_mathjax (line 461) | pub fn copy_mathjax() -> impl BuildAction {
function build_sass (line 470) | fn build_sass(build: &mut Build) -> Result<()> {
FILE: build/ninja_gen/src/action.rs
type BuildAction (line 9) | pub trait BuildAction {
method command (line 11) | fn command(&self) -> &str;
method files (line 14) | fn files(&mut self, build: &mut impl FilesHandle);
method check_output_timestamps (line 19) | fn check_output_timestamps(&self) -> bool {
method generator (line 24) | fn generator(&self) -> bool {
method on_first_instance (line 31) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
method concurrency_pool (line 35) | fn concurrency_pool(&self) -> Option<&'static str> {
method bypass_runner (line 39) | fn bypass_runner(&self) -> bool {
method hide_success (line 43) | fn hide_success(&self) -> bool {
method hide_progress (line 47) | fn hide_progress(&self) -> bool {
method name (line 51) | fn name(&self) -> &'static str {
method command (line 67) | fn command(&self) -> &str {
method files (line 70) | fn files(&mut self, _build: &mut impl FilesHandle) {}
type TestBuildAction (line 63) | trait TestBuildAction {}
function should_strip_regions_in_type_name (line 75) | fn should_strip_regions_in_type_name() {
FILE: build/ninja_gen/src/archives.rs
type OnlineArchive (line 17) | pub struct OnlineArchive {
type Platform (line 23) | pub enum Platform {
method current (line 33) | pub fn current() -> Self {
method tls_feature (line 47) | pub fn tls_feature() -> &'static str {
method as_rust_triple (line 57) | pub fn as_rust_triple(&self) -> &'static str {
function with_exe (line 70) | pub fn with_exe(path: &str) -> Cow<'_, str> {
type DownloadArchive (line 78) | struct DownloadArchive {
method command (line 83) | fn command(&self) -> &str {
method files (line 87) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method check_output_timestamps (line 96) | fn check_output_timestamps(&self) -> bool {
type ExtractArchive (line 101) | struct ExtractArchive<'a, I> {
function extraction_folder (line 116) | fn extraction_folder(&self) -> Utf8PathBuf {
method command (line 128) | fn command(&self) -> &str {
method files (line 132) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method name (line 149) | fn name(&self) -> &'static str {
method check_output_timestamps (line 153) | fn check_output_timestamps(&self) -> bool {
function download_and_extract (line 159) | pub fn download_and_extract<I>(
function empty_manifest (line 184) | pub fn empty_manifest() -> HashMap<&'static str, &'static [&'static str]> {
FILE: build/ninja_gen/src/bin/update_node.rs
type NodeRelease (line 13) | struct NodeRelease {
type NodeFile (line 19) | struct NodeFile {
function main (line 24) | fn main() -> Result<(), Box<dyn Error>> {
function fetch_node_release_info (line 32) | fn fetch_node_release_info() -> Result<NodeRelease, Box<dyn Error>> {
function generate_node_archive_function (line 125) | fn generate_node_archive_function(release: &NodeRelease) -> Result<Strin...
function update_node_text (line 190) | fn update_node_text(new_function: &str) -> Result<(), Box<dyn Error>> {
function read_node_rs (line 205) | fn read_node_rs() -> Result<String, Box<dyn Error>> {
function write_node_rs (line 213) | fn write_node_rs(content: &str) -> Result<(), Box<dyn Error>> {
function test_regex_replacement (line 227) | fn test_regex_replacement() {
FILE: build/ninja_gen/src/bin/update_protoc.rs
function fetch_protoc_release_info (line 14) | fn fetch_protoc_release_info() -> Result<String, Box<dyn Error>> {
function read_protobuf_rs (line 89) | fn read_protobuf_rs() -> Result<String, Box<dyn Error>> {
function update_protoc_text (line 97) | fn update_protoc_text(old_text: &str, new_protoc_text: &str) -> Result<S...
function write_protobuf_rs (line 110) | fn write_protobuf_rs(content: &str) -> Result<(), Box<dyn Error>> {
function main (line 118) | fn main() -> Result<(), Box<dyn Error>> {
FILE: build/ninja_gen/src/bin/update_uv.rs
function fetch_uv_release_info (line 12) | fn fetch_uv_release_info() -> Result<String, Box<dyn Error>> {
function read_python_rs (line 84) | fn read_python_rs() -> Result<String, Box<dyn Error>> {
function update_uv_text (line 92) | fn update_uv_text(old_text: &str, new_uv_text: &str) -> Result<String, B...
function write_python_rs (line 103) | fn write_python_rs(content: &str) -> Result<(), Box<dyn Error>> {
function main (line 111) | fn main() -> Result<(), Box<dyn Error>> {
function test_update_uv_text_with_actual_file (line 125) | fn test_update_uv_text_with_actual_file() {
FILE: build/ninja_gen/src/build.rs
type Build (line 19) | pub struct Build {
method new (line 34) | pub fn new() -> Result<Self> {
method variable (line 60) | pub fn variable(&mut self, name: &'static str, value: impl Into<String...
method pool (line 64) | pub fn pool(&mut self, name: &'static str, size: usize) {
method once_only (line 70) | pub fn once_only(
method add_action (line 82) | pub fn add_action(&mut self, group: impl AsRef<str>, action: impl Buil...
method add_resolved_files_to_group (line 143) | fn add_resolved_files_to_group<'a>(
method add_dependency (line 156) | pub fn add_dependency(&mut self, group: &str, deps: BuildInput) {
method group_outputs (line 166) | pub fn group_outputs(&self, group_name: &'static str) -> &[String] {
method group_output (line 174) | pub fn group_output(&self, group_name: &'static str) -> String {
method expand_inputs (line 180) | pub fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<Str...
method filter_inputs (line 185) | pub fn filter_inputs<F>(&self, inputs: impl AsRef<BuildInput>, func: F...
method inputs_with_suffix (line 195) | pub fn inputs_with_suffix(&self, inputs: impl AsRef<BuildInput>, ext: ...
function split_groups (line 200) | fn split_groups(group: &str) -> Vec<&str> {
type BuildStatement (line 210) | struct BuildStatement<'a> {
function from_build_action (line 233) | fn from_build_action<'a>(
function render_into (line 289) | fn render_into(mut self, buf: &mut String) -> (Vec<String>, Vec<(String,...
function prepare_command (line 314) | fn prepare_command(&mut self, command: String) -> Result<String> {
function expand_inputs (line 340) | fn expand_inputs(
type BuildProfile (line 350) | pub enum BuildProfile {
method from_env (line 357) | fn from_env() -> Self {
type FilesHandle (line 366) | pub trait FilesHandle {
method add_inputs (line 373) | fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<Bu...
method add_inputs_vec (line 374) | fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<Strin...
method add_order_only_inputs (line 375) | fn add_order_only_inputs(&mut self, variable: &'static str, inputs: im...
method add_variable (line 378) | fn add_variable(&mut self, name: impl Into<String>, value: impl Into<S...
method expand_input (line 380) | fn expand_input(&self, input: &BuildInput) -> String;
method expand_inputs (line 381) | fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String>;
method add_outputs (line 384) | fn add_outputs(
method add_outputs_ext (line 403) | fn add_outputs_ext(
method add_output_stamp (line 413) | fn add_output_stamp(&mut self, path: impl Into<String>);
method add_env_var (line 418) | fn add_env_var(&mut self, key: &str, constant_value: &str);
method set_working_dir (line 423) | fn set_working_dir(&mut self, constant_value: &str);
method create_dir_all (line 427) | fn create_dir_all(&mut self, key: &str, path: impl Into<String>);
method build_profile (line 429) | fn build_profile(&self) -> BuildProfile;
method add_inputs (line 433) | fn add_inputs(&mut self, variable: &'static str, inputs: impl AsRef<Bu...
method add_inputs_vec (line 437) | fn add_inputs_vec(&mut self, variable: &'static str, inputs: Vec<Strin...
method add_order_only_inputs (line 449) | fn add_order_only_inputs(&mut self, variable: &'static str, inputs: im...
method add_variable (line 457) | fn add_variable(&mut self, key: impl Into<String>, value: impl Into<St...
method expand_input (line 461) | fn expand_input(&self, input: &BuildInput) -> String {
method add_outputs_ext (line 470) | fn add_outputs_ext(
method expand_inputs (line 508) | fn expand_inputs(&self, inputs: impl AsRef<BuildInput>) -> Vec<String> {
method build_profile (line 512) | fn build_profile(&self) -> BuildProfile {
method add_output_stamp (line 516) | fn add_output_stamp(&mut self, path: impl Into<String>) {
method add_env_var (line 521) | fn add_env_var(&mut self, key: &str, constant_value: &str) {
method set_working_dir (line 525) | fn set_working_dir(&mut self, constant_value: &str) {
method create_dir_all (line 529) | fn create_dir_all(&mut self, key: &str, path: impl Into<String>) {
function to_ninja_target_string (line 536) | fn to_ninja_target_string(
function test_split_groups (line 558) | fn test_split_groups() {
FILE: build/ninja_gen/src/cargo.rs
type RustOutput (line 17) | pub enum RustOutput<'a> {
function name (line 26) | pub fn name(&self) -> &str {
function path (line 35) | pub fn path(
function profile_output_dir (line 70) | fn profile_output_dir(profile: BuildProfile) -> &'static str {
type CargoBuild (line 79) | pub struct CargoBuild<'a> {
method command (line 88) | fn command(&self) -> &str {
method files (line 92) | fn files(&mut self, build: &mut impl FilesHandle) {
method check_output_timestamps (line 120) | fn check_output_timestamps(&self) -> bool {
method on_first_instance (line 124) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
function profile_arg_for_cargo (line 129) | fn profile_arg_for_cargo(profile: BuildProfile) -> Option<&'static str> {
function setup_flags (line 137) | fn setup_flags(build: &mut Build) -> Result<()> {
type CargoTest (line 144) | pub struct CargoTest {
method command (line 149) | fn command(&self) -> &str {
method files (line 153) | fn files(&mut self, build: &mut impl FilesHandle) {
method on_first_instance (line 160) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
type CargoClippy (line 172) | pub struct CargoClippy {
method command (line 177) | fn command(&self) -> &str {
method files (line 181) | fn files(&mut self, build: &mut impl FilesHandle) {
method on_first_instance (line 189) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
type CargoFormat (line 194) | pub struct CargoFormat {
method command (line 201) | fn command(&self) -> &str {
method files (line 205) | fn files(&mut self, build: &mut impl FilesHandle) {
method on_first_instance (line 218) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
type CargoInstall (line 225) | pub struct CargoInstall {
method command (line 232) | fn command(&self) -> &str {
method files (line 236) | fn files(&mut self, build: &mut impl FilesHandle) {
method check_output_timestamps (line 241) | fn check_output_timestamps(&self) -> bool {
type CargoRun (line 246) | pub struct CargoRun {
method command (line 254) | fn command(&self) -> &str {
method files (line 258) | fn files(&mut self, build: &mut impl FilesHandle) {
FILE: build/ninja_gen/src/command.rs
type RunCommand (line 11) | pub struct RunCommand<'a> {
method command (line 21) | fn command(&self) -> &str {
method files (line 25) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
FILE: build/ninja_gen/src/configure.rs
type ConfigureBuild (line 15) | pub struct ConfigureBuild {}
method command (line 18) | fn command(&self) -> &str {
method files (line 22) | fn files(&mut self, build: &mut impl FilesHandle) {
method on_first_instance (line 29) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
method generator (line 43) | fn generator(&self) -> bool {
method check_output_timestamps (line 47) | fn check_output_timestamps(&self) -> bool {
FILE: build/ninja_gen/src/copy.rs
type CopyFiles (line 12) | pub struct CopyFiles<'a> {
method command (line 20) | fn command(&self) -> &str {
method files (line 25) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
type CopyFile (line 40) | pub struct CopyFile<'a> {
method command (line 46) | fn command(&self) -> &str {
method files (line 50) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
type LinkFile (line 59) | pub struct LinkFile<'a> {
method command (line 65) | fn command(&self) -> &str {
method files (line 73) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method check_output_timestamps (line 78) | fn check_output_timestamps(&self) -> bool {
FILE: build/ninja_gen/src/git.rs
type SyncSubmodule (line 11) | pub struct SyncSubmodule {
method command (line 17) | fn command(&self) -> &str {
method files (line 25) | fn files(&mut self, build: &mut impl build::FilesHandle) {
method on_first_instance (line 38) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
method concurrency_pool (line 43) | fn concurrency_pool(&self) -> Option<&'static str> {
function locate_git_head (line 51) | fn locate_git_head() -> Option<BuildInput> {
FILE: build/ninja_gen/src/hash.rs
function simple_hash (line 8) | pub fn simple_hash(hashable: impl Hash) -> u64 {
FILE: build/ninja_gen/src/input.rs
type BuildInput (line 11) | pub enum BuildInput {
method as_ref (line 21) | fn as_ref(&self) -> &BuildInput {
method from (line 27) | fn from(v: String) -> Self {
method from (line 33) | fn from(v: &str) -> Self {
method from (line 39) | fn from(v: Vec<String>) -> Self {
method from (line 45) | fn from(v: Glob) -> Self {
method from (line 51) | fn from(v: &BuildInput) -> Self {
method from (line 57) | fn from(v: &[BuildInput]) -> Self {
method from (line 63) | fn from(v: Vec<BuildInput>) -> Self {
method from (line 69) | fn from(v: Utf8PathBuf) -> Self {
method add_to_vec (line 75) | pub fn add_to_vec(
type Glob (line 117) | pub struct Glob {
method resolve (line 148) | pub fn resolve(&self) -> impl Iterator<Item = Utf8PathBuf> {
function cache_files (line 126) | fn cache_files() -> Vec<Utf8PathBuf> {
function space_separated (line 176) | pub fn space_separated<I>(iter: I) -> String
FILE: build/ninja_gen/src/node.rs
function node_archive (line 19) | pub fn node_archive(platform: Platform) -> OnlineArchive {
type YarnSetup (line 48) | pub struct YarnSetup {}
method command (line 51) | fn command(&self) -> &str {
method files (line 59) | fn files(&mut self, build: &mut impl build::FilesHandle) {
method check_output_timestamps (line 72) | fn check_output_timestamps(&self) -> bool {
type YarnInstall (line 76) | pub struct YarnInstall<'a> {
method command (line 82) | fn command(&self) -> &str {
method files (line 86) | fn files(&mut self, build: &mut impl build::FilesHandle) {
method check_output_timestamps (line 96) | fn check_output_timestamps(&self) -> bool {
function with_cmd_ext (line 101) | fn with_cmd_ext(bin: &str) -> Cow<'_, str> {
function setup_node (line 109) | pub fn setup_node(
type EsbuildScript (line 167) | pub struct EsbuildScript<'a> {
method command (line 178) | fn command(&self) -> &str {
method files (line 182) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type DPrint (line 195) | pub struct DPrint {
method command (line 201) | fn command(&self) -> &str {
method files (line 205) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type Prettier (line 214) | pub struct Prettier {
method command (line 220) | fn command(&self) -> &str {
method files (line 224) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type SvelteCheck (line 239) | pub struct SvelteCheck {
method command (line 245) | fn command(&self) -> &str {
method files (line 249) | fn files(&mut self, build: &mut impl build::FilesHandle) {
method hide_progress (line 259) | fn hide_progress(&self) -> bool {
type TypescriptCheck (line 264) | pub struct TypescriptCheck {
method command (line 270) | fn command(&self) -> &str {
method files (line 274) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type Eslint (line 284) | pub struct Eslint<'a> {
method command (line 292) | fn command(&self) -> &str {
method files (line 296) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type ViteTest (line 309) | pub struct ViteTest {
method command (line 314) | fn command(&self) -> &str {
method files (line 318) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type SqlFormat (line 326) | pub struct SqlFormat {
method command (line 332) | fn command(&self) -> &str {
method files (line 336) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type GenTypescriptProto (line 346) | pub struct GenTypescriptProto<'a> {
method command (line 358) | fn command(&self) -> &str {
method files (line 364) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type CompileSass (line 397) | pub struct CompileSass<'a> {
method command (line 405) | fn command(&self) -> &str {
method files (line 409) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type CompileTypescript (line 425) | pub struct CompileTypescript<'a> {
method command (line 434) | fn command(&self) -> &str {
method files (line 438) | fn files(&mut self, build: &mut impl build::FilesHandle) {
type SveltekitBuild (line 463) | pub struct SveltekitBuild {
method command (line 469) | fn command(&self) -> &str {
method files (line 477) | fn files(&mut self, build: &mut impl build::FilesHandle) {
FILE: build/ninja_gen/src/protobuf.rs
function protoc_archive (line 20) | pub fn protoc_archive(platform: Platform) -> OnlineArchive {
function clang_format_archive (line 49) | fn clang_format_archive(platform: Platform) -> OnlineArchive {
type ClangFormat (line 78) | pub struct ClangFormat {
method command (line 84) | fn command(&self) -> &str {
method files (line 87) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method on_first_instance (line 99) | fn on_first_instance(&self, build: &mut crate::Build) -> anyhow::Result<...
function setup_protoc (line 112) | pub fn setup_protoc(build: &mut Build) -> Result<()> {
function check_proto (line 137) | pub fn check_proto(build: &mut Build, inputs: BuildInput) -> Result<()> {
FILE: build/ninja_gen/src/python.rs
function uv_archive (line 26) | pub fn uv_archive(platform: Platform) -> OnlineArchive {
function setup_uv (line 67) | pub fn setup_uv(build: &mut Build, platform: Platform) -> Result<()> {
type PythonEnvironment (line 116) | pub struct PythonEnvironment {
method command (line 125) | fn command(&self) -> &str {
method files (line 133) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method check_output_timestamps (line 169) | fn check_output_timestamps(&self) -> bool {
type PythonTypecheck (line 174) | pub struct PythonTypecheck {
method command (line 180) | fn command(&self) -> &str {
method files (line 184) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method hide_progress (line 194) | fn hide_progress(&self) -> bool {
type PythonFormat (line 199) | struct PythonFormat<'a> {
method command (line 205) | fn command(&self) -> &str {
method files (line 209) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
function python_format (line 223) | pub fn python_format(build: &mut Build, group: &str, inputs: BuildInput)...
type RuffCheck (line 242) | pub struct RuffCheck {
method command (line 249) | fn command(&self) -> &str {
method files (line 253) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
type PythonTest (line 273) | pub struct PythonTest {
method command (line 280) | fn command(&self) -> &str {
method files (line 284) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method hide_progress (line 298) | fn hide_progress(&self) -> bool {
FILE: build/ninja_gen/src/render.rs
method render (line 16) | pub fn render(&self) -> String {
method write_build_file (line 60) | pub fn write_build_file(&self) -> Result<()> {
FILE: build/ninja_gen/src/rsync.rs
type RsyncFiles (line 16) | pub struct RsyncFiles<'a> {
method command (line 24) | fn command(&self) -> &str {
method files (line 28) | fn files(&mut self, build: &mut impl FilesHandle) {
method check_output_timestamps (line 67) | fn check_output_timestamps(&self) -> bool {
FILE: build/ninja_gen/src/sass.rs
type CompileSassWithGrass (line 13) | pub struct CompileSassWithGrass {
method command (line 21) | fn command(&self) -> &str {
method files (line 25) | fn files(&mut self, build: &mut impl crate::build::FilesHandle) {
method on_first_instance (line 35) | fn on_first_instance(&self, build: &mut Build) -> Result<()> {
FILE: build/runner/build.rs
function main (line 4) | fn main() {
FILE: build/runner/src/archive.rs
type ArchiveArgs (line 18) | pub enum ArchiveArgs {
type DownloadArgs (line 24) | pub struct DownloadArgs {
type ExtractArgs (line 31) | pub struct ExtractArgs {
function archive_command (line 37) | pub async fn archive_command(args: ArchiveArgs) -> Result<()> {
function download_and_check (line 46) | async fn download_and_check(archive_url: &str, checksum: &str, output_pa...
function sha2_data (line 64) | fn sha2_data(data: &[u8]) -> String {
type CompressionKind (line 71) | enum CompressionKind {
type ArchiveKind (line 79) | enum ArchiveKind {
function extract_archive (line 84) | fn extract_archive(archive_path: &str, output_folder: &str) -> Result<()> {
FILE: build/runner/src/build.rs
type BuildArgs (line 22) | pub struct BuildArgs {
function run_build (line 27) | pub fn run_build(args: BuildArgs) {
function get_ninja_command (line 117) | fn get_ninja_command() -> &'static str {
function setup_build_root (line 125) | fn setup_build_root() -> Utf8PathBuf {
function bootstrap_build (line 154) | fn bootstrap_build() {
function maybe_update_buildhash (line 161) | fn maybe_update_buildhash(build_root: &Utf8Path) {
function get_buildhash (line 169) | fn get_buildhash() -> String {
function write_if_changed (line 180) | fn write_if_changed(path: &Utf8Path, contents: &str) {
function maybe_update_env_file (line 190) | fn maybe_update_env_file(build_root: &Utf8Path) {
FILE: build/runner/src/main.rs
type Cli (line 33) | struct Cli {
type Command (line 39) | enum Command {
function main (line 49) | fn main() -> Result<()> {
FILE: build/runner/src/paths.rs
function absolute_msys_path (line 8) | pub fn absolute_msys_path(path: &Utf8Path) -> String {
FILE: build/runner/src/pyenv.rs
type PyenvArgs (line 13) | pub struct PyenvArgs {
function setup_pyenv (line 23) | pub fn setup_pyenv(args: PyenvArgs) {
FILE: build/runner/src/rsync.rs
type RsyncArgs (line 13) | pub struct RsyncArgs {
function rsync_files (line 24) | pub fn rsync_files(args: RsyncArgs) {
FILE: build/runner/src/run.rs
type RunArgs (line 13) | pub struct RunArgs {
function run_commands (line 28) | pub fn run_commands(args: RunArgs) -> Result<()> {
function split_env (line 42) | fn split_env(s: &str) -> Result<(String, String), std::io::Error> {
function build_command (line 50) | fn build_command(
function split_args (line 67) | fn split_args(args: Vec<String>) -> Vec<Vec<String>> {
function run_command (line 84) | pub fn run_command(command: &mut Command) {
FILE: build/runner/src/yarn.rs
type YarnArgs (line 13) | pub struct YarnArgs {
function setup_yarn (line 18) | pub fn setup_yarn(args: YarnArgs) {
function link_node_modules (line 44) | fn link_node_modules() {
function link_node_modules (line 61) | fn link_node_modules() {
FILE: ftl/src/garbage_collection.rs
type WriteJsonArgs (line 25) | pub struct WriteJsonArgs {
type GarbageCollectArgs (line 31) | pub struct GarbageCollectArgs {
type DeprecateEntriesArgs (line 37) | pub struct DeprecateEntriesArgs {
constant DEPCRATION_WARNING (line 46) | const DEPCRATION_WARNING: &str =
function write_ftl_json (line 52) | pub fn write_ftl_json(args: WriteJsonArgs) -> Result<()> {
function garbage_collect_ftl_entries (line 64) | pub fn garbage_collect_ftl_entries(args: GarbageCollectArgs) -> Result<(...
function deprecate_ftl_entries (line 73) | pub fn deprecate_ftl_entries(args: DeprecateEntriesArgs) -> Result<()> {
function get_all_used_messages_and_terms (line 81) | fn get_all_used_messages_and_terms(
function for_files_with_ending (line 91) | fn for_files_with_ending(
function gather_ftl_references (line 112) | fn gather_ftl_references(roots: &[impl AsRef<str>]) -> HashSet<String> {
function rewrite_ftl_files (line 122) | fn rewrite_ftl_files(
function import_messages_from_json (line 135) | fn import_messages_from_json(json_roots: &[impl AsRef<str>], entries: &m...
function extract_nested_messages_and_terms (line 143) | fn extract_nested_messages_and_terms(
function strip_unused_ftl_messages_and_terms (line 157) | fn strip_unused_ftl_messages_and_terms(roots: &[impl AsRef<str>], used_f...
function deprecate_unused_ftl_messages_and_terms (line 165) | fn deprecate_unused_ftl_messages_and_terms(roots: &[impl AsRef<str>], us...
function append_deprecation_warning (line 179) | fn append_deprecation_warning(entries: &mut Vec<ast::Entry<&str>>) {
function entry_use_check (line 191) | fn entry_use_check(used_ftls: &HashSet<String>) -> impl Fn(&ast::Entry<&...
function extract_references_from_file (line 199) | fn extract_references_from_file(refs: &mut HashSet<String>, entry: &DirE...
function snake_to_kebab_case (line 230) | fn snake_to_kebab_case(name: &str) -> String {
function camel_to_kebab_case (line 234) | fn camel_to_kebab_case(name: &str) -> String {
function case_conversion (line 252) | fn case_conversion() {
FILE: ftl/src/main.rs
type Cli (line 23) | struct Cli {
type Command (line 29) | enum Command {
function main (line 50) | fn main() -> Result<()> {
FILE: ftl/src/serialize.rs
function serialize (line 13) | pub fn serialize<'s, S: Slice<'s>>(resource: &Resource<S>) -> String {
function serialize_with_options (line 17) | pub fn serialize_with_options<'s, S: Slice<'s>>(
type Serializer (line 30) | pub struct Serializer {
method new (line 37) | pub fn new(options: Options) -> Self {
method serialize_resource (line 45) | pub fn serialize_resource<'s, S: Slice<'s>>(&mut self, res: &Resource<...
method into_serialized_text (line 65) | pub fn into_serialized_text(self) -> String {
method serialize_junk (line 69) | fn serialize_junk(&mut self, junk: &str) -> Result<(), Error> {
method serialize_free_comment (line 73) | fn serialize_free_comment<'s, S: Slice<'s>>(
method serialize_comment (line 87) | fn serialize_comment<'s, S: Slice<'s>>(
method serialize_message (line 106) | fn serialize_message<'s, S: Slice<'s>>(&mut self, msg: &Message<S>) ->...
method serialize_term (line 124) | fn serialize_term<'s, S: Slice<'s>>(&mut self, term: &Term<S>) -> Resu...
method serialize_pattern (line 141) | fn serialize_pattern<'s, S: Slice<'s>>(&mut self, pattern: &Pattern<S>...
method serialize_attributes (line 165) | fn serialize_attributes<'s, S: Slice<'s>>(
method serialize_attribute (line 185) | fn serialize_attribute<'s, S: Slice<'s>>(&mut self, attr: &Attribute<S...
method serialize_element (line 195) | fn serialize_element<'s, S: Slice<'s>>(
method serialize_expression (line 228) | fn serialize_expression<'s, S: Slice<'s>>(
method serialize_inline_expression (line 240) | fn serialize_inline_expression<'s, S: Slice<'s>>(
method serialize_select_expression (line 303) | fn serialize_select_expression<'s, S: Slice<'s>>(
method serialize_variant (line 323) | fn serialize_variant<'s, S: Slice<'s>>(&mut self, variant: &Variant<S>...
method serialize_variant_key (line 336) | fn serialize_variant_key<'s, S: Slice<'s>>(
method serialize_call_arguments (line 347) | fn serialize_call_arguments<'s, S: Slice<'s>>(
function is_select_expr (line 380) | fn is_select_expr<'s, S: Slice<'s>>(expr: &Expression<S>) -> bool {
type Options (line 391) | pub struct Options {
type State (line 396) | struct State {
type TextWriter (line 401) | struct TextWriter {
method indent (line 407) | fn indent(&mut self) {
method dedent (line 411) | fn dedent(&mut self) {
method write_indent (line 418) | fn write_indent(&mut self) {
method newline (line 424) | fn newline(&mut self) {
method write_literal (line 428) | fn write_literal(&mut self, mut item: &str) -> fmt::Result {
method write_char_into_indent (line 441) | fn write_char_into_indent(&mut self, ch: char) {
FILE: ftl/src/string/copy.rs
type CopyOrMoveArgs (line 15) | pub struct CopyOrMoveArgs {
type CopyOrMove (line 27) | pub(super) enum CopyOrMove {
function copy_or_move (line 32) | pub(super) fn copy_or_move(mode: CopyOrMove, args: CopyOrMoveArgs) -> an...
FILE: ftl/src/string/mod.rs
type StringCommand (line 33) | pub enum StringCommand {
function string_operation (line 46) | pub fn string_operation(args: StringCommand) -> anyhow::Result<()> {
function additional_template_folder (line 53) | fn additional_template_folder(dst_folder: &Utf8Path) -> Option<Utf8PathB...
function all_langs (line 68) | fn all_langs(lang_folder: &Utf8Path) -> Result<Vec<Utf8PathBuf>> {
function ftl_file_from_key (line 76) | fn ftl_file_from_key(old_key: &str) -> String {
function parse_file (line 97) | fn parse_file(ftl_path: &Utf8Path) -> Result<Resource<String>> {
function serialize_file (line 108) | fn serialize_file(path: &Utf8Path, resource: &Resource<String>) -> Resul...
function get_entry (line 118) | fn get_entry(fname: &Utf8Path, key: &str) -> Option<Entry<String>> {
function write_entry (line 131) | fn write_entry(path: &Utf8Path, key: &str, mut entry: Entry<String>) -> ...
function delete_entry (line 148) | fn delete_entry(path: &Utf8Path, key: &str) -> Result<bool> {
FILE: ftl/src/string/transform.rs
type TransformArgs (line 25) | pub struct TransformArgs {
type TransformTarget (line 39) | pub enum TransformTarget {
function transform (line 44) | pub fn transform(args: TransformArgs) -> Result<()> {
function transform_ftl (line 61) | fn transform_ftl(ftl: &Utf8Path, regex: &Regex, args: &TransformArgs) ->...
function transform_ftl_inner (line 70) | fn transform_ftl_inner(
function transform_pattern (line 88) | fn transform_pattern(pattern: &mut Pattern<String>, regex: &Regex, args:...
function transform_variable (line 103) | fn transform_variable(
function transform_text (line 135) | fn transform_text(
function transform (line 179) | fn transform() -> Result<()> {
FILE: ftl/src/sync.rs
type Module (line 13) | struct Module {
constant GIT_REMOTE (line 20) | const GIT_REMOTE: &str = "ssh";
function sync (line 22) | pub fn sync() -> Result<()> {
function check_clean (line 43) | fn check_clean() -> Result<()> {
function fetch_new_translations (line 57) | fn fetch_new_translations(module: &Module) -> Result<()> {
function push_new_templates (line 69) | fn push_new_templates(module: &Module) -> Result<()> {
function push (line 90) | fn push(repo: &Utf8Path) -> Result<()> {
function commit (line 103) | fn commit<F>(folder: F, message: &str) -> Result<()>
FILE: pylib/anki/_backend.py
class RustBackend (line 56) | class RustBackend(RustBackendGenerated):
method initialize_logging (line 71) | def initialize_logging(path: str | None = None) -> None:
method __init__ (line 74) | def __init__(
method syncserver (line 92) | def syncserver() -> None:
method db_query (line 95) | def db_query(
method db_execute_many (line 102) | def db_execute_many(self, sql: str, args: list[list[ValueForDB]]) -> l...
method db_begin (line 105) | def db_begin(self) -> None:
method db_commit (line 108) | def db_commit(self) -> None:
method db_rollback (line 111) | def db_rollback(self) -> None:
method _db_command (line 114) | def _db_command(self, input: dict[str, Any]) -> Any:
method translate (line 124) | def translate(
method format_time_span (line 140) | def format_time_span(
method compute_params_from_items (line 151) | def compute_params_from_items(self, items: Iterable[FsrsItem]) -> Sequ...
method benchmark (line 154) | def benchmark(self, train_set: Iterable[FsrsItem]) -> Sequence[float]:
method _run_command (line 157) | def _run_command(self, service: int, method: int, input: bytes) -> bytes:
class Translations (line 174) | class Translations(GeneratedTranslations):
method __init__ (line 175) | def __init__(self, backend: ref[RustBackend] | None):
method __call__ (line 178) | def __call__(self, key: tuple[int, int], **kwargs: Any) -> str:
method _translate (line 189) | def _translate(
function backend_exception_to_pylib (line 197) | def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception:
FILE: pylib/anki/_legacy.py
function _target_to_string (line 22) | def _target_to_string(target: DeprecatedAliasTarget | None) -> str:
function partial_path (line 30) | def partial_path(full_path: str, components: int) -> str:
function print_deprecation_warning (line 35) | def print_deprecation_warning(msg: str, frame: int = 1) -> None:
function _print_warning (line 44) | def _print_warning(old: str, doc: str, frame: int = 1) -> None:
function _print_replacement_warning (line 48) | def _print_replacement_warning(old: str, new: str, frame: int = 1) -> None:
function _get_remapped_and_replacement (line 53) | def _get_remapped_and_replacement(
class DeprecatedNamesMixin (line 65) | class DeprecatedNamesMixin:
method __getattr__ (line 76) | def __getattr__(self, name: str) -> Any:
method register_deprecated_aliases (line 89) | def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) ...
method register_deprecated_attributes (line 99) | def register_deprecated_attributes(
class DeprecatedNamesMixinForModule (line 120) | class DeprecatedNamesMixinForModule:
method __init__ (line 136) | def __init__(self, module_globals: dict[str, Any]) -> None:
method __getattr__ (line 143) | def __getattr__(self, name: str) -> Any:
method register_deprecated_aliases (line 156) | def register_deprecated_aliases(self, **kwargs: DeprecatedAliasTarget)...
method register_deprecated_attributes (line 159) | def register_deprecated_attributes(
function deprecated (line 169) | def deprecated(replaced_by: Callable | None = None, info: str = "") -> C...
function deprecated_keywords (line 187) | def deprecated_keywords(**replaced_keys: str) -> Callable:
FILE: pylib/anki/_rsbridge.pyi
class Backend (line 3) | class Backend:
method command (line 5) | def command(cls, service: int, method: int, data: bytes) -> bytes: ...
method db_command (line 6) | def db_command(self, data: bytes) -> bytes: ...
function buildhash (line 8) | def buildhash() -> str: ...
function open_backend (line 9) | def open_backend(data: bytes) -> Backend: ...
function initialize_logging (line 10) | def initialize_logging(log_file: Union[str, None]) -> Backend: ...
function syncserver (line 11) | def syncserver() -> None: ...
FILE: pylib/anki/_vendor/stringcase.py
function camelcase (line 12) | def camelcase(string):
function capitalcase (line 31) | def capitalcase(string):
function constcase (line 49) | def constcase(string):
function lowercase (line 64) | def lowercase(string):
function pascalcase (line 78) | def pascalcase(string):
function pathcase (line 92) | def pathcase(string):
function backslashcase (line 109) | def backslashcase(string):
function sentencecase (line 126) | def sentencecase(string):
function snakecase (line 150) | def snakecase(string):
function spinalcase (line 170) | def spinalcase(string):
function dotcase (line 185) | def dotcase(string):
function titlecase (line 200) | def titlecase(string):
function trimcase (line 216) | def trimcase(string):
function uppercase (line 229) | def uppercase(string):
function alphanumcase (line 243) | def alphanumcase(string):
FILE: pylib/anki/browser.py
class BrowserConfig (line 5) | class BrowserConfig:
method active_columns_key (line 14) | def active_columns_key(is_notes_mode: bool) -> str:
method sort_column_key (line 20) | def sort_column_key(is_notes_mode: bool) -> str:
method sort_backwards_key (line 26) | def sort_backwards_key(is_notes_mode: bool) -> str:
class BrowserDefaults (line 32) | class BrowserDefaults:
FILE: pylib/anki/cards.py
class Card (line 39) | class Card(DeprecatedNamesMixin):
method __init__ (line 54) | def __init__(
method load (line 73) | def load(self) -> None:
method _load_from_backend_card (line 78) | def _load_from_backend_card(self, card: cards_pb2.Card) -> None:
method _to_backend_card (line 113) | def _to_backend_card(self) -> cards_pb2.Card:
method flush (line 140) | def flush(self) -> None:
method question (line 149) | def question(self, reload: bool = False, browser: bool = False) -> str:
method answer (line 152) | def answer(self) -> str:
method question_av_tags (line 155) | def question_av_tags(self) -> list[AVTag]:
method answer_av_tags (line 158) | def answer_av_tags(self) -> list[AVTag]:
method render_output (line 161) | def render_output(
method set_render_output (line 172) | def set_render_output(self, output: anki.template.TemplateRenderOutput...
method note (line 175) | def note(self, reload: bool = False) -> Note:
method note_type (line 180) | def note_type(self) -> NotetypeDict:
method template (line 183) | def template(self) -> TemplateDict:
method start_timer (line 191) | def start_timer(self) -> None:
method current_deck_id (line 194) | def current_deck_id(self) -> anki.decks.DeckId:
method time_limit (line 197) | def time_limit(self) -> int:
method should_show_timer (line 202) | def should_show_timer(self) -> bool:
method replay_question_audio_on_answer_side (line 206) | def replay_question_audio_on_answer_side(self) -> bool:
method autoplay (line 210) | def autoplay(self) -> bool:
method time_taken (line 215) | def time_taken(self, capped: bool = True) -> int:
method description (line 223) | def description(self) -> str:
method user_flag (line 232) | def user_flag(self) -> int:
method set_user_flag (line 235) | def set_user_flag(self, flag: int) -> None:
method css (line 242) | def css(self) -> str:
method is_empty (line 246) | def is_empty(self) -> bool:
FILE: pylib/anki/collection.py
class DeckIdLimit (line 103) | class DeckIdLimit:
class NoteIdsLimit (line 108) | class NoteIdsLimit:
class CardIdsLimit (line 113) | class CardIdsLimit:
class ComputedMemoryState (line 121) | class ComputedMemoryState:
class AddNoteRequest (line 129) | class AddNoteRequest:
class Collection (line 134) | class Collection(DeprecatedNamesMixin):
method initialize_backend_logging (line 138) | def initialize_backend_logging() -> None:
method __init__ (line 142) | def __init__(
method name (line 163) | def name(self) -> Any:
method weakref (line 166) | def weakref(self) -> Collection:
method backend (line 171) | def backend(self) -> RustBackend:
method format_timespan (line 182) | def format_timespan(
method latest_progress (line 192) | def latest_progress(self) -> Progress:
method sched_ver (line 200) | def sched_ver(self) -> Literal[1, 2]:
method _load_scheduler (line 210) | def _load_scheduler(self) -> None:
method upgrade_to_v2_scheduler (line 225) | def upgrade_to_v2_scheduler(self) -> None:
method v3_scheduler (line 229) | def v3_scheduler(self) -> bool:
method set_v3_scheduler (line 232) | def set_v3_scheduler(self, enabled: bool) -> None:
method crt (line 245) | def crt(self) -> int:
method crt (line 249) | def crt(self, crt: int) -> None:
method mod (line 253) | def mod(self) -> int:
method save (line 257) | def save(self, **args: Any) -> None:
method autosave (line 261) | def autosave(self) -> None:
method close (line 264) | def close(
method close_for_full_sync (line 276) | def close_for_full_sync(self) -> None:
method _clear_caches (line 282) | def _clear_caches(self) -> None:
method reopen (line 285) | def reopen(self, after_full_sync: bool = False) -> None:
method set_schema_modified (line 302) | def set_schema_modified(self) -> None:
method mod_schema (line 305) | def mod_schema(self, check: bool) -> None:
method schema_changed (line 312) | def schema_changed(self) -> bool:
method usn (line 316) | def usn(self) -> int:
method create_backup (line 325) | def create_backup(
method await_backup_completion (line 349) | def await_backup_completion(self) -> None:
method export_collection_package (line 353) | def export_collection_package(
method import_anki_package (line 361) | def import_anki_package(
method export_anki_package (line 367) | def export_anki_package(
method get_csv_metadata (line 376) | def get_csv_metadata(self, path: str, delimiter: Delimiter.V | None) -...
method import_csv (line 380) | def import_csv(self, request: ImportCsvRequest) -> ImportLogWithChanges:
method export_note_csv (line 384) | def export_note_csv(
method export_card_csv (line 405) | def export_card_csv(
method import_json_file (line 418) | def import_json_file(self, path: str) -> ImportLogWithChanges:
method import_json_string (line 421) | def import_json_string(self, json: str) -> ImportLogWithChanges:
method export_dataset_for_research (line 424) | def export_dataset_for_research(
method get_image_for_occlusion (line 432) | def get_image_for_occlusion(self, path: str | None) -> GetImageForOccl...
method add_image_occlusion_notetype (line 435) | def add_image_occlusion_notetype(self) -> None:
method add_image_occlusion_note (line 439) | def add_image_occlusion_note(
method get_image_occlusion_note (line 457) | def get_image_occlusion_note(
method update_image_occlusion_note (line 462) | def update_image_occlusion_note(
method get_card (line 481) | def get_card(self, id: CardId | None) -> Card:
method update_cards (line 484) | def update_cards(
method update_card (line 492) | def update_card(self, card: Card, skip_undo_entry: bool = False) -> Op...
method get_note (line 496) | def get_note(self, id: NoteId) -> Note:
method update_notes (line 499) | def update_notes(
method update_note (line 507) | def update_note(self, note: Note, skip_undo_entry: bool = False) -> Op...
method nextID (line 514) | def nextID(self, type: str, inc: bool = True) -> Any:
method reset (line 522) | def reset(self) -> None:
method new_note (line 528) | def new_note(self, notetype: NotetypeDict) -> Note:
method add_note (line 531) | def add_note(self, note: Note, deck_id: DeckId) -> OpChangesWithCount:
method add_notes (line 537) | def add_notes(self, requests: Iterable[AddNoteRequest]) -> OpChanges:
method remove_notes (line 553) | def remove_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount:
method remove_notes_by_card (line 557) | def remove_notes_by_card(self, card_ids: list[CardId]) -> None:
method card_ids_of_note (line 565) | def card_ids_of_note(self, note_id: NoteId) -> Sequence[CardId]:
method defaults_for_adding (line 568) | def defaults_for_adding(
method default_deck_for_notetype (line 584) | def default_deck_for_notetype(self, notetype_id: NotetypeId) -> DeckId...
method note_count (line 599) | def note_count(self) -> int:
method is_empty (line 605) | def is_empty(self) -> bool:
method card_count (line 608) | def card_count(self) -> Any:
method remove_cards_and_orphaned_notes (line 611) | def remove_cards_and_orphaned_notes(
method set_deck (line 617) | def set_deck(self, card_ids: Sequence[CardId], deck_id: int) -> OpChan...
method get_empty_cards (line 620) | def get_empty_cards(self) -> EmptyCardsReport:
method after_note_updates (line 626) | def after_note_updates(
method find_cards (line 637) | def find_cards(
method find_notes (line 669) | def find_notes(
method _build_sort_mode (line 685) | def _build_sort_mode(
method find_and_replace (line 714) | def find_and_replace(
method field_names_for_note_ids (line 734) | def field_names_for_note_ids(self, nids: Sequence[int]) -> Sequence[str]:
method find_dupes (line 738) | def find_dupes(self, field_name: str, search: str = "") -> list[tuple[...
method build_search_string (line 776) | def build_search_string(
method group_searches (line 792) | def group_searches(
method join_searches (line 819) | def join_searches(
method replace_in_search_node (line 838) | def replace_in_search_node(
method _pb_search_separator (line 849) | def _pb_search_separator(self, operator: SearchJoiner) -> SearchNode.G...
method all_browser_columns (line 858) | def all_browser_columns(self) -> Sequence[BrowserColumns.Column]:
method get_browser_column (line 861) | def get_browser_column(self, key: str) -> BrowserColumns.Column | None:
method browser_row_for_id (line 867) | def browser_row_for_id(
method load_browser_card_columns (line 883) | def load_browser_card_columns(self) -> list[str]:
method set_browser_card_columns (line 891) | def set_browser_card_columns(self, columns: list[str]) -> None:
method load_browser_note_columns (line 895) | def load_browser_note_columns(self) -> list[str]:
method set_browser_note_columns (line 903) | def set_browser_note_columns(self, columns: list[str]) -> None:
method get_config (line 910) | def get_config(self, key: str, default: Any | None = None) -> Any:
method set_config (line 916) | def set_config(self, key: str, val: Any, *, undoable: bool = False) ->...
method remove_config (line 928) | def remove_config(self, key: str) -> OpChanges:
method all_config (line 931) | def all_config(self) -> dict[str, Any]:
method get_config_bool (line 935) | def get_config_bool(self, key: Config.Bool.V) -> bool:
method set_config_bool (line 938) | def set_config_bool(
method get_config_string (line 943) | def get_config_string(self, key: Config.String.V) -> str:
method set_config_string (line 946) | def set_config_string(
method get_aux_notetype_config (line 951) | def get_aux_notetype_config(
method set_aux_notetype_config (line 957) | def set_aux_notetype_config(
method get_aux_template_config (line 963) | def get_aux_template_config(
method set_aux_template_config (line 971) | def set_aux_template_config(
method _get_load_balancer_enabled (line 985) | def _get_load_balancer_enabled(self) -> bool:
method _set_load_balancer_enabled (line 988) | def _set_load_balancer_enabled(self, value: bool) -> None:
method _get_enable_fsrs_short_term_with_steps (line 995) | def _get_enable_fsrs_short_term_with_steps(self) -> bool:
method _set_enable_fsrs_short_term_with_steps (line 998) | def _set_enable_fsrs_short_term_with_steps(self, value: bool) -> None:
method stats (line 1008) | def stats(self) -> anki.stats.CollectionStats:
method card_stats_data (line 1013) | def card_stats_data(self, card_id: CardId) -> stats_pb2.CardStatsRespo...
method get_review_logs (line 1022) | def get_review_logs(
method studied_today (line 1027) | def studied_today(self) -> str:
method undo_status (line 1033) | def undo_status(self) -> UndoStatus:
method add_custom_undo_entry (line 1037) | def add_custom_undo_entry(self, name: str) -> int:
method merge_undo_entries (line 1049) | def merge_undo_entries(self, target: int) -> OpChanges:
method undo (line 1059) | def undo(self) -> OpChangesAfterUndo:
method redo (line 1066) | def redo(self) -> OpChangesAfterUndo:
method op_made_changes (line 1073) | def op_made_changes(self, changes: OpChanges) -> bool:
method _check_backend_undo_status (line 1080) | def _check_backend_undo_status(self) -> UndoStatus | None:
method fix_integrity (line 1092) | def fix_integrity(self) -> tuple[str, bool]:
method optimize (line 1107) | def optimize(self) -> None:
method set_user_flag_for_cards (line 1113) | def set_user_flag_for_cards(
method set_wants_abort (line 1118) | def set_wants_abort(self) -> None:
method i18n_resources (line 1121) | def i18n_resources(self, modules: Sequence[str]) -> bytes:
method abort_media_sync (line 1124) | def abort_media_sync(self) -> None:
method abort_sync (line 1127) | def abort_sync(self) -> None:
method full_upload_or_download (line 1130) | def full_upload_or_download(
method sync_login (line 1139) | def sync_login(
method sync_collection (line 1146) | def sync_collection(self, auth: SyncAuth, sync_media: bool) -> SyncOut...
method sync_media (line 1149) | def sync_media(self, auth: SyncAuth) -> None:
method sync_status (line 1152) | def sync_status(self, auth: SyncAuth) -> SyncStatus:
method media_sync_status (line 1155) | def media_sync_status(self) -> MediaSyncStatus:
method ankihub_login (line 1159) | def ankihub_login(self, id: str, password: str) -> str:
method ankihub_logout (line 1162) | def ankihub_logout(self, token: str) -> None:
method get_preferences (line 1165) | def get_preferences(self) -> Preferences:
method set_preferences (line 1168) | def set_preferences(self, prefs: Preferences) -> OpChanges:
method render_markdown (line 1171) | def render_markdown(self, text: str, sanitize: bool = True) -> str:
method compare_answer (line 1175) | def compare_answer(
method extract_cloze_for_typing (line 1182) | def extract_cloze_for_typing(self, text: str, ordinal: int) -> str:
method compute_memory_state (line 1185) | def compute_memory_state(self, card_id: CardId) -> ComputedMemoryState:
method fuzz_delta (line 1200) | def fuzz_delta(self, card_id: CardId, interval: int) -> int:
method startTimebox (line 1214) | def startTimebox(self) -> None:
method timeboxReached (line 1218) | def timeboxReached(self) -> Literal[False] | tuple[Any, int]:
method log (line 1232) | def log(self, *args: Any, **kwargs: Any) -> None:
method undo_name (line 1236) | def undo_name(self) -> str | None:
method newNote (line 1242) | def newNote(self, forDeck: bool = True) -> Note:
method addNote (line 1247) | def addNote(self, note: Note) -> int:
method remNotes (line 1252) | def remNotes(self, ids: Sequence[NoteId]) -> None:
method _remNotes (line 1256) | def _remNotes(self, ids: list[NoteId]) -> None:
method card_stats (line 1260) | def card_stats(self, card_id: CardId, include_revlog: bool) -> str:
method cardStats (line 1266) | def cardStats(self, card: Card) -> str:
method updateFieldCache (line 1272) | def updateFieldCache(self, nids: list[NoteId]) -> None:
method genCards (line 1276) | def genCards(self, nids: list[NoteId]) -> list[int]:
method emptyCids (line 1282) | def emptyCids(self) -> list[CardId]:
method _logRem (line 1286) | def _logRem(self, ids: list[int | NoteId], type: int) -> None:
method setMod (line 1293) | def setMod(self) -> None:
method flush (line 1297) | def flush(self) -> None:
function pb_export_limit (line 1311) | def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit:
FILE: pylib/anki/config.py
class ConfigManager (line 37) | class ConfigManager:
method __init__ (line 38) | def __init__(self, col: anki.collection.Collection):
method get_immutable (line 41) | def get_immutable(self, key: str) -> Any:
method set (line 47) | def set(self, key: str, val: Any) -> None:
method remove (line 55) | def remove(self, key: str) -> OpChanges:
method __getitem__ (line 61) | def __getitem__(self, key: str) -> Any:
method __setitem__ (line 76) | def __setitem__(self, key: str, value: Any) -> None:
method get (line 79) | def get(self, key: str, default: Any | None = None) -> Any:
method setdefault (line 85) | def setdefault(self, key: str, default: Any) -> Any:
method __contains__ (line 90) | def __contains__(self, key: str) -> bool:
method __delitem__ (line 97) | def __delitem__(self, key: str) -> None:
class WrappedList (line 109) | class WrappedList(list):
method __init__ (line 110) | def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
method __del__ (line 116) | def __del__(self) -> None:
class WrappedDict (line 123) | class WrappedDict(dict):
method __init__ (line 124) | def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
method __del__ (line 130) | def __del__(self) -> None:
FILE: pylib/anki/consts.py
function _tr (line 100) | def _tr(col: anki.collection.Collection | None) -> Any:
function new_card_order_labels (line 113) | def new_card_order_labels(col: anki.collection.Collection | None) -> dic...
function __getattr__ (line 126) | def __getattr__(name: str) -> Any:
FILE: pylib/anki/db.py
class DB (line 26) | class DB(DeprecatedNamesMixin):
method __init__ (line 27) | def __init__(self, path: str, timeout: int = 0) -> None:
method __repr__ (line 34) | def __repr__(self) -> str:
method execute (line 39) | def execute(self, sql: str, *a: Any, **ka: Any) -> Cursor:
method executemany (line 59) | def executemany(self, sql: str, iterable: Any) -> None:
method commit (line 68) | def commit(self) -> None:
method executescript (line 74) | def executescript(self, sql: str) -> None:
method rollback (line 80) | def rollback(self) -> None:
method scalar (line 83) | def scalar(self, *a: Any, **kw: Any) -> Any:
method all (line 89) | def all(self, *a: Any, **kw: Any) -> list:
method first (line 92) | def first(self, *a: Any, **kw: Any) -> Any:
method list (line 98) | def list(self, *a: Any, **kw: Any) -> list:
method close (line 101) | def close(self) -> None:
method set_progress_handler (line 105) | def set_progress_handler(self, *args: Any) -> None:
method __enter__ (line 108) | def __enter__(self) -> "DB":
method __exit__ (line 112) | def __exit__(self, *args: Any) -> None:
method total_changes (line 115) | def total_changes(self) -> Any:
method interrupt (line 118) | def interrupt(self) -> None:
method set_autocommit (line 121) | def set_autocommit(self, autocommit: bool) -> None:
method _text_factory (line 128) | def _text_factory(self, data: bytes) -> str:
method cursor (line 131) | def cursor(self, factory: type[Cursor] = Cursor) -> Cursor:
FILE: pylib/anki/dbproxy.py
class DBProxy (line 24) | class DBProxy:
method __init__ (line 28) | def __init__(self, backend: anki._backend.RustBackend) -> None:
method transact (line 34) | def transact(self, op: Callable[[], None]) -> None:
method _query (line 57) | def _query(
method all (line 71) | def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> li...
method list (line 74) | def list(
method first (line 79) | def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> ...
method scalar (line 86) | def scalar(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) ->...
method executemany (line 100) | def executemany(self, sql: str, args: Iterable[Sequence[ValueForDB]]) ...
function emulate_named_args (line 109) | def emulate_named_args(
FILE: pylib/anki/decks.py
class DecksDictProxy (line 42) | class DecksDictProxy:
method __init__ (line 43) | def __init__(self, col: anki.collection.Collection):
method _warn (line 46) | def _warn(self) -> None:
method __getitem__ (line 51) | def __getitem__(self, item: Any) -> Any:
method __setitem__ (line 55) | def __setitem__(self, key: Any, val: Any) -> None:
method __len__ (line 59) | def __len__(self) -> int:
method keys (line 63) | def keys(self) -> Any:
method values (line 67) | def values(self) -> Any:
method items (line 71) | def items(self) -> Any:
method __contains__ (line 75) | def __contains__(self, item: Any) -> bool:
class DeckManager (line 80) | class DeckManager(DeprecatedNamesMixin):
method __init__ (line 84) | def __init__(self, col: anki.collection.Collection) -> None:
method save (line 88) | def save(self, deck_or_config: DeckDict | DeckConfigDict | None = None...
method add_normal_deck_with_name (line 104) | def add_normal_deck_with_name(self, name: str) -> OpChangesWithId:
method add_deck_legacy (line 113) | def add_deck_legacy(self, deck: DeckDict) -> OpChangesWithId:
method id (line 119) | def id(
method remove (line 137) | def remove(self, dids: Sequence[DeckId]) -> OpChangesWithCount:
method all_names_and_ids (line 140) | def all_names_and_ids(
method id_for_name (line 148) | def id_for_name(self, name: str) -> DeckId | None:
method get_legacy (line 154) | def get_legacy(self, did: DeckId) -> DeckDict | None:
method have (line 160) | def have(self, id: DeckId) -> bool:
method get_all_legacy (line 163) | def get_all_legacy(self) -> list[DeckDict]:
method new_deck (line 166) | def new_deck(self) -> Deck:
method add_deck (line 170) | def add_deck(self, deck: Deck) -> OpChangesWithId:
method new_deck_legacy (line 173) | def new_deck_legacy(self, filtered: bool) -> DeckDict:
method deck_tree (line 185) | def deck_tree(self) -> DeckTreeNode:
method find_deck_in_tree (line 189) | def find_deck_in_tree(
method all (line 200) | def all(self) -> list[DeckDict]:
method set_collapsed (line 204) | def set_collapsed(
method collapse (line 211) | def collapse(self, did: DeckId) -> None:
method collapse_browser (line 216) | def collapse_browser(self, did: DeckId) -> None:
method count (line 222) | def count(self) -> int:
method card_count (line 225) | def card_count(
method get (line 240) | def get(self, did: DeckId | str, default: bool = True) -> DeckDict | N...
method by_name (line 255) | def by_name(self, name: str) -> DeckDict | None:
method update (line 262) | def update(self, deck: DeckDict, preserve_usn: bool = True) -> None:
method update_dict (line 268) | def update_dict(self, deck: DeckDict) -> OpChanges:
method rename (line 271) | def rename(self, deck: DeckDict | DeckId, new_name: str) -> OpChanges:
method reparent (line 282) | def reparent(
method get_deck_configs_for_update (line 294) | def get_deck_configs_for_update(self, deck_id: DeckId) -> DeckConfigsF...
method update_deck_configs (line 297) | def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
method all_config (line 301) | def all_config(self) -> list[DeckConfigDict]:
method config_dict_for_deck_id (line 305) | def config_dict_for_deck_id(self, did: DeckId) -> DeckConfigDict:
method get_config (line 319) | def get_config(self, conf_id: DeckConfigId) -> DeckConfigDict | None:
method update_config (line 325) | def update_config(self, conf: DeckConfigDict, preserve_usn: bool = Fal...
method add_config (line 331) | def add_config(
method add_config_returning_id (line 343) | def add_config_returning_id(
method remove_config (line 348) | def remove_config(self, id: DeckConfigId) -> None:
method set_config_id_for_deck_dict (line 360) | def set_config_id_for_deck_dict(self, deck: DeckDict, id: DeckConfigId...
method decks_using_config (line 364) | def decks_using_config(self, conf: DeckConfigDict) -> list[DeckId]:
method restore_to_default (line 371) | def restore_to_default(self, conf: DeckConfigDict) -> None:
method name (line 384) | def name(self, did: DeckId, default: bool = False) -> str:
method name_if_exists (line 390) | def name_if_exists(self, did: DeckId) -> str | None:
method cids (line 396) | def cids(self, did: DeckId, children: bool = False) -> list[anki.cards...
method for_card_ids (line 404) | def for_card_ids(self, cids: list[anki.cards.CardId]) -> list[DeckId]:
method set_current (line 410) | def set_current(self, deck: DeckId) -> OpChanges:
method get_current_id (line 413) | def get_current_id(self) -> DeckId:
method current (line 417) | def current(self) -> DeckDict:
method active (line 420) | def active(self) -> list[DeckId]:
method select (line 424) | def select(self, did: DeckId) -> None:
method path (line 435) | def path(name: str) -> list[str]:
method basename (line 439) | def basename(cls, name: str) -> str:
method immediate_parent_path (line 443) | def immediate_parent_path(cls, name: str) -> list[str]:
method immediate_parent (line 447) | def immediate_parent(cls, name: str) -> str | None:
method key (line 454) | def key(cls, deck: DeckDict) -> list[str]:
method deck_and_child_name_ids (line 457) | def deck_and_child_name_ids(self, deck_id: DeckId) -> Iterable[tuple[s...
method children (line 464) | def children(self, did: DeckId) -> list[tuple[str, DeckId]]:
method child_ids (line 472) | def child_ids(self, parent_name: str) -> Iterable[DeckId]:
method deck_and_child_ids (line 477) | def deck_and_child_ids(self, deck_id: DeckId) -> list[DeckId]:
method parents (line 483) | def parents(
method parents_by_name (line 504) | def parents_by_name(self, name: str) -> list[DeckDict]:
method new_filtered (line 523) | def new_filtered(self, name: str) -> DeckId:
method is_filtered (line 529) | def is_filtered(self, did: DeckId | str) -> bool:
method flush (line 536) | def flush(self) -> None:
method rem (line 540) | def rem(
method name_map (line 551) | def name_map(self) -> dict[str, DeckDict]:
method set_deck (line 555) | def set_deck(self, cids: list[anki.cards.CardId], did: DeckId) -> None:
method all_ids (line 565) | def all_ids(self) -> list[str]:
method all_names (line 569) | def all_names(self, dyn: bool = True, force_default: bool = True) -> l...
function __getattr__ (line 595) | def __getattr__(name):
FILE: pylib/anki/errors.py
class AnkiException (line 13) | class AnkiException(Exception):
class BackendError (line 28) | class BackendError(AnkiException):
method __init__ (line 31) | def __init__(
method __str__ (line 44) | def __str__(self) -> str:
class Interrupted (line 48) | class Interrupted(BackendError):
class NetworkError (line 52) | class NetworkError(BackendError):
class SyncErrorKind (line 56) | class SyncErrorKind(Enum):
class SyncError (line 61) | class SyncError(BackendError):
method __init__ (line 62) | def __init__(
class BackendIOError (line 74) | class BackendIOError(BackendError):
class CustomStudyError (line 78) | class CustomStudyError(BackendError):
class DBError (line 82) | class DBError(BackendError):
class CardTypeError (line 86) | class CardTypeError(BackendError):
class TemplateError (line 90) | class TemplateError(BackendError):
class NotFoundError (line 94) | class NotFoundError(BackendError):
class DeletedError (line 98) | class DeletedError(BackendError):
class ExistsError (line 102) | class ExistsError(BackendError):
class UndoEmpty (line 106) | class UndoEmpty(BackendError):
class FilteredDeckError (line 110) | class FilteredDeckError(BackendError):
class InvalidInput (line 114) | class InvalidInput(BackendError):
class SearchError (line 118) | class SearchError(BackendError):
class SchedulerUpgradeRequired (line 122) | class SchedulerUpgradeRequired(BackendError):
class AbortSchemaModification (line 126) | class AbortSchemaModification(AnkiException):
FILE: pylib/anki/exporting.py
class Exporter (line 27) | class Exporter:
method __init__ (line 34) | def __init__(
method key (line 45) | def key(col: Collection) -> str:
method doExport (line 48) | def doExport(self, path) -> None:
method exportInto (line 51) | def exportInto(self, path: str) -> None:
method processText (line 57) | def processText(self, text: str) -> str:
method escapeText (line 65) | def escapeText(self, text: str) -> str:
method stripHTML (line 78) | def stripHTML(self, text: str) -> str:
method cardIds (line 88) | def cardIds(self) -> Any:
class TextCardExporter (line 103) | class TextCardExporter(Exporter):
method __init__ (line 107) | def __init__(self, col) -> None:
method key (line 111) | def key(col: Collection) -> str:
method doExport (line 114) | def doExport(self, file) -> None:
class TextNoteExporter (line 135) | class TextNoteExporter(Exporter):
method __init__ (line 140) | def __init__(self, col: Collection) -> None:
method key (line 145) | def key(col: Collection) -> str:
method doExport (line 148) | def doExport(self, file: BufferedWriter) -> None:
class AnkiExporter (line 179) | class AnkiExporter(Exporter):
method __init__ (line 184) | def __init__(self, col: Collection) -> None:
method key (line 188) | def key(col: Collection) -> str:
method deckIds (line 191) | def deckIds(self) -> list[DeckId]:
method exportInto (line 199) | def exportInto(self, path: str) -> None:
method postExport (line 305) | def postExport(self) -> None:
method removeSystemTags (line 310) | def removeSystemTags(self, tags: str) -> str:
method _modelHasMedia (line 313) | def _modelHasMedia(self, model, fname) -> bool:
class AnkiPackageExporter (line 328) | class AnkiPackageExporter(AnkiExporter):
method __init__ (line 331) | def __init__(self, col: Collection) -> None:
method key (line 335) | def key(col: Collection) -> str:
method exportInto (line 338) | def exportInto(self, path: str) -> None:
method doExport (line 348) | def doExport(self, z: ZipFile, path: str) -> dict[str, str]: # type: ...
method _exportMedia (line 368) | def _exportMedia(self, z: ZipFile, files: list[str], fdir: str) -> dic...
method prepareMedia (line 386) | def prepareMedia(self) -> None:
method _addDummyCollection (line 393) | def _addDummyCollection(self, zip) -> None:
class AnkiCollectionPackageExporter (line 409) | class AnkiCollectionPackageExporter(AnkiPackageExporter):
method __init__ (line 415) | def __init__(self, col):
method key (line 419) | def key(col: Collection) -> str:
method exportInto (line 422) | def exportInto(self, path: str) -> None:
class AnkiCollectionPackage21bExporter (line 442) | class AnkiCollectionPackage21bExporter(AnkiCollectionPackageExporter):
method key (line 446) | def key(_col: Collection) -> str:
function exporters (line 454) | def exporters(col: Collection) -> list[tuple[str, Any]]:
FILE: pylib/anki/find.py
class Finder (line 15) | class Finder:
method __init__ (line 16) | def __init__(self, col: Collection | None) -> None:
method findCards (line 20) | def findCards(self, query: Any, order: Any) -> Any:
method findNotes (line 23) | def findNotes(self, query: Any) -> Any:
function findReplace (line 31) | def findReplace(
function fieldNamesForNotes (line 52) | def fieldNamesForNotes(col: Collection, nids: list[NoteId]) -> list[str]:
function fieldNames (line 60) | def fieldNames(col: Collection, downcase: bool = True) -> list[str]:
FILE: pylib/anki/foreign_data/__init__.py
class ForeignCardType (line 18) | class ForeignCardType:
method front_back (line 24) | def front_back() -> ForeignCardType:
method back_front (line 32) | def back_front() -> ForeignCardType:
method cloze (line 40) | def cloze() -> ForeignCardType:
class ForeignNotetype (line 47) | class ForeignNotetype:
method basic (line 54) | def basic(name: str) -> ForeignNotetype:
method basic_reverse (line 58) | def basic_reverse(name: str) -> ForeignNotetype:
method cloze (line 66) | def cloze(name: str) -> ForeignNotetype:
class ForeignCard (line 73) | class ForeignCard:
class ForeignNote (line 93) | class ForeignNote:
class ForeignData (line 102) | class ForeignData:
method serialize (line 107) | def serialize(self) -> str:
class ForeignDataEncoder (line 111) | class ForeignDataEncoder(json.JSONEncoder):
method default (line 112) | def default(self, obj: object) -> dict:
FILE: pylib/anki/foreign_data/mnemosyne.py
function serialize (line 32) | def serialize(db_path: str, deck_id: DeckId) -> str:
function gather_data (line 37) | def gather_data(db: DB, deck_id: DeckId) -> ForeignData:
function open_mnemosyne_db (line 46) | def open_mnemosyne_db(db_path: str) -> DB:
class MnemoFactView (line 54) | class MnemoFactView(ABC):
method foreign_notetype (line 60) | def foreign_notetype(cls) -> ForeignNotetype:
class FrontOnly (line 64) | class FrontOnly(MnemoFactView):
method foreign_notetype (line 69) | def foreign_notetype(cls) -> ForeignNotetype:
class FrontBack (line 73) | class FrontBack(MnemoFactView):
method foreign_notetype (line 78) | def foreign_notetype(cls) -> ForeignNotetype:
class Vocabulary (line 82) | class Vocabulary(MnemoFactView):
method foreign_notetype (line 87) | def foreign_notetype(cls) -> ForeignNotetype:
method _recognition_card_type (line 95) | def _recognition_card_type() -> ForeignCardType:
method _production_card_type (line 104) | def _production_card_type() -> ForeignCardType:
class Cloze (line 113) | class Cloze(MnemoFactView):
method foreign_notetype (line 118) | def foreign_notetype(cls) -> ForeignNotetype:
class MnemoCard (line 123) | class MnemoCard:
method card_ord (line 132) | def card_ord(self) -> int:
method is_new (line 141) | def is_new(self) -> bool:
method foreign_card (line 144) | def foreign_card(self) -> ForeignCard:
method anki_interval (line 153) | def anki_interval(self) -> int:
class MnemoFact (line 158) | class MnemoFact:
method foreign_note (line 163) | def foreign_note(
method fact_view (line 175) | def fact_view(self) -> type[MnemoFactView]:
method anki_fields (line 192) | def anki_fields(self, fact_view: type[MnemoFactView]) -> list[str]:
method anki_tags (line 195) | def anki_tags(self) -> list[str]:
method foreign_cards (line 206) | def foreign_cards(self) -> list[ForeignCard]:
function munge_field (line 211) | def munge_field(field: str) -> str:
function gather_facts (line 221) | def gather_facts(db: DB) -> dict[int, MnemoFact]:
function gather_cards_into_facts (line 235) | def gather_cards_into_facts(db: DB, facts: dict[int, MnemoFact]) -> None:
FILE: pylib/anki/hooks.py
function runHook (line 30) | def runHook(hook: str, *args: Any) -> None:
function runFilter (line 42) | def runFilter(hook: str, arg: Any, *args: Any) -> Any:
function addHook (line 54) | def addHook(hook: str, func: Callable) -> None:
function remHook (line 62) | def remHook(hook: Any, func: Any) -> None:
function wrap (line 77) | def wrap(old: Any, new: Any, pos: str = "after") -> Callable:
FILE: pylib/anki/httpclient.py
class HttpClient (line 25) | class HttpClient(DeprecatedNamesMixin):
method __init__ (line 31) | def __init__(self, progress_hook: ProgressCallback | None = None) -> N...
method __enter__ (line 35) | def __enter__(self) -> HttpClient:
method __exit__ (line 38) | def __exit__(self, *args: Any) -> None:
method close (line 41) | def close(self) -> None:
method __del__ (line 46) | def __del__(self) -> None:
method post (line 49) | def post(self, url: str, data: bytes, headers: dict[str, str] | None) ...
method get (line 60) | def get(self, url: str, headers: dict[str, str] | None = None) -> Resp...
method stream_content (line 68) | def stream_content(self, resp: Response) -> bytes:
method _agent_name (line 78) | def _agent_name(self) -> str:
FILE: pylib/anki/importing/__init__.py
function importers (line 17) | def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
FILE: pylib/anki/importing/anki2.py
class V2ImportIntoV1 (line 25) | class V2ImportIntoV1(Exception):
class MediaMapInvalid (line 29) | class MediaMapInvalid(Exception):
class Anki2Importer (line 33) | class Anki2Importer(Importer):
method __init__ (line 40) | def __init__(self, col: Collection, file: str) -> None:
method run (line 47) | def run(self, media: None = None, importing_v2: bool = True) -> None:
method _prepareFiles (line 58) | def _prepareFiles(self) -> None:
method _import (line 71) | def _import(self) -> None:
method _logNoteRow (line 87) | def _logNoteRow(self, action: str, noteRow: list[str]) -> None:
method _importNotes (line 92) | def _importNotes(self) -> None:
method _uniquifyNote (line 201) | def _uniquifyNote(self, note: list[Any]) -> bool:
method _prepareModels (line 223) | def _prepareModels(self) -> None:
method _mid (line 227) | def _mid(self, srcMid: NotetypeId) -> Any:
method _did (line 264) | def _did(self, did: DeckId) -> Any:
method _importCards (line 311) | def _importCards(self) -> None:
method _importStaticMedia (line 403) | def _importStaticMedia(self) -> None:
method _mediaData (line 413) | def _mediaData(self, fname: str, dir: str | None = None) -> bytes:
method _srcMediaData (line 423) | def _srcMediaData(self, fname: str) -> bytes:
method _dstMediaData (line 427) | def _dstMediaData(self, fname: str) -> bytes:
method _writeDstMedia (line 431) | def _writeDstMedia(self, fname: str, data: bytes) -> None:
method _mungeMedia (line 440) | def _mungeMedia(self, mid: NotetypeId, fieldsStr: str) -> str:
method _postImport (line 472) | def _postImport(self) -> None:
FILE: pylib/anki/importing/apkg.py
class AnkiPackageImporter (line 17) | class AnkiPackageImporter(Anki2Importer):
method run (line 21) | def run(self) -> None: # type: ignore
method _srcMediaData (line 61) | def _srcMediaData(self, fname: str) -> Any:
FILE: pylib/anki/importing/base.py
class Importer (line 16) | class Importer:
method __init__ (line 21) | def __init__(self, col: Collection, file: str) -> None:
method run (line 28) | def run(self) -> None:
method open (line 31) | def open(self) -> None:
method close (line 35) | def close(self) -> None:
method _prepareTS (line 45) | def _prepareTS(self) -> None:
method ts (line 48) | def ts(self) -> Any:
FILE: pylib/anki/importing/csvfile.py
class TextImporter (line 15) | class TextImporter(NoteImporter):
method __init__ (line 19) | def __init__(self, col: Collection, file: str) -> None:
method foreignNotes (line 29) | def foreignNotes(self) -> list[ForeignNote]:
method open (line 62) | def open(self) -> None:
method cacheFile (line 67) | def cacheFile(self) -> None:
method openFile (line 72) | def openFile(self) -> None:
method updateDelimiter (line 92) | def updateDelimiter(self) -> None:
method fields (line 132) | def fields(self) -> int:
method close (line 137) | def close(self):
method __del__ (line 142) | def __del__(self):
method noteFromFields (line 148) | def noteFromFields(self, fields: list[str]) -> ForeignNote:
FILE: pylib/anki/importing/mnemo.py
class MnemosyneImporter (line 14) | class MnemosyneImporter(NoteImporter):
method run (line 19) | def run(self):
method fields (line 106) | def fields(self):
method _mungeField (line 109) | def _mungeField(self, fld):
method _addFronts (line 118) | def _addFronts(self, notes, model=None, fields=("f", "b")):
method _addFrontBacks (line 143) | def _addFrontBacks(self, notes):
method _addVocabulary (line 153) | def _addVocabulary(self, notes):
method _addCloze (line 174) | def _addCloze(self, notes):
FILE: pylib/anki/importing/noteimp.py
class ForeignNote (line 35) | class ForeignNote:
method __init__ (line 38) | def __init__(self) -> None:
class ForeignCard (line 46) | class ForeignCard:
method __init__ (line 47) | def __init__(self) -> None:
class NoteImporter (line 73) | class NoteImporter(Importer):
method __init__ (line 81) | def __init__(self, col: Collection, file: str) -> None:
method run (line 88) | def run(self) -> None:
method fields (line 94) | def fields(self) -> int:
method initMapping (line 98) | def initMapping(self) -> None:
method mappingOk (line 109) | def mappingOk(self) -> bool:
method foreignNotes (line 112) | def foreignNotes(self) -> list:
method importNotes (line 116) | def importNotes(self, notes: list[ForeignNote]) -> None:
method newData (line 234) | def newData(
method addNew (line 258) | def addNew(
method updateData (line 268) | def updateData(
method addUpdates (line 292) | def addUpdates(self, rows: list[Updates]) -> None:
method processFields (line 318) | def processFields(self, note: ForeignNote, fields: list[str] | None = ...
method updateCards (line 335) | def updateCards(self) -> None:
FILE: pylib/anki/lang.py
function lang_to_disk_lang (line 136) | def lang_to_disk_lang(lang: str) -> str:
function _ (line 176) | def _(str: str) -> str:
function ngettext (line 181) | def ngettext(single: str, plural: str, num: int) -> str:
function set_lang (line 186) | def set_lang(lang: str) -> None:
function get_def_lang (line 193) | def get_def_lang(user_lang: str | None = None) -> tuple[int, str]:
function is_rtl (line 239) | def is_rtl(lang: str) -> bool:
function without_unicode_isolation (line 245) | def without_unicode_isolation(string: str) -> str:
function with_collapsed_whitespace (line 249) | def with_collapsed_whitespace(string: str) -> str:
function __getattr__ (line 258) | def __getattr__(name: str) -> Any:
FILE: pylib/anki/latex.py
class ExtractedLatex (line 45) | class ExtractedLatex:
class ExtractedLatexOutput (line 51) | class ExtractedLatexOutput:
method from_proto (line 56) | def from_proto(
function on_card_did_render (line 68) | def on_card_did_render(
function render_latex (line 77) | def render_latex(
function render_latex_returning_errors (line 87) | def render_latex_returning_errors(
function _save_latex_image (line 121) | def _save_latex_image(
function _err_msg (line 164) | def _err_msg(col: anki.collection.Collection, type: str, texpath: str) -...
function setup_hook (line 178) | def setup_hook() -> None:
FILE: pylib/anki/media.py
function media_paths_from_col_path (line 23) | def media_paths_from_col_path(col_path: str) -> tuple[str, str]:
class MediaManager (line 32) | class MediaManager(DeprecatedNamesMixin):
method __init__ (line 46) | def __init__(self, col: anki.collection.Collection, server: bool) -> N...
method __repr__ (line 55) | def __repr__(self) -> str:
method dir (line 60) | def dir(self) -> str:
method force_resync (line 63) | def force_resync(self) -> None:
method empty_trash (line 69) | def empty_trash(self) -> None:
method restore_trash (line 72) | def restore_trash(self) -> None:
method strip_av_tags (line 75) | def strip_av_tags(self, text: str) -> str:
method _extract_filenames (line 78) | def _extract_filenames(self, text: str) -> list[str]:
method add_file (line 90) | def add_file(self, path: str) -> str:
method write_data (line 97) | def write_data(self, desired_fname: str, data: bytes) -> str:
method add_extension_based_on_mime (line 103) | def add_extension_based_on_mime(self, fname: str, content_type: str) -...
method have (line 125) | def have(self, fname: str) -> bool:
method trash_files (line 128) | def trash_files(self, fnames: list[str]) -> None:
method files_in_str (line 136) | def files_in_str(
method extract_static_media_files (line 152) | def extract_static_media_files(self, mid: NotetypeId) -> Sequence[str]:
method transform_names (line 155) | def transform_names(self, txt: str, func: Callable) -> str:
method strip (line 160) | def strip(self, txt: str) -> str:
method escape_images (line 166) | def escape_images(self, string: str, unescape: bool = False) -> str:
method escape_media_filenames (line 170) | def escape_media_filenames(self, string: str, unescape: bool = False) ...
method check (line 180) | def check(self) -> CheckMediaResponse:
method render_all_latex (line 184) | def render_all_latex(
method _legacy_strip_illegal (line 220) | def _legacy_strip_illegal(self, str: str) -> str:
method _legacy_has_illegal (line 224) | def _legacy_has_illegal(self, string: str) -> bool:
method _legacy_find_changes (line 233) | def _legacy_find_changes(self) -> None:
method _legacy_write_data (line 237) | def _legacy_write_data(
FILE: pylib/anki/models.py
class ModelsDictProxy (line 42) | class ModelsDictProxy:
method __init__ (line 43) | def __init__(self, col: anki.collection.Collection):
method _warn (line 46) | def _warn(self) -> None:
method __getitem__ (line 51) | def __getitem__(self, item: Any) -> Any:
method __setitem__ (line 55) | def __setitem__(self, key: str, val: Any) -> None:
method __len__ (line 59) | def __len__(self) -> int:
method keys (line 63) | def keys(self) -> Any:
method values (line 67) | def values(self) -> Any:
method items (line 71) | def items(self) -> Any:
method __contains__ (line 75) | def __contains__(self, item: Any) -> bool:
class ModelManager (line 80) | class ModelManager(DeprecatedNamesMixin):
method __init__ (line 84) | def __init__(self, col: anki.collection.Collection) -> None:
method __repr__ (line 90) | def __repr__(self) -> str:
method _update_cache (line 104) | def _update_cache(self, notetype: NotetypeDict) -> None:
method _remove_from_cache (line 107) | def _remove_from_cache(self, ntid: NotetypeId) -> None:
method _get_cached (line 111) | def _get_cached(self, ntid: NotetypeId) -> NotetypeDict | None:
method _clear_cache (line 114) | def _clear_cache(self) -> None:
method all_names_and_ids (line 120) | def all_names_and_ids(self) -> Sequence[NotetypeNameId]:
method all_use_counts (line 123) | def all_use_counts(self) -> Sequence[NotetypeNameIdUseCount]:
method have (line 127) | def have(self, id: NotetypeId) -> bool:
method current (line 135) | def current(self, for_deck: bool = True) -> NotetypeDict:
method id_for_name (line 147) | def id_for_name(self, name: str) -> NotetypeId | None:
method get (line 153) | def get(self, id: NotetypeId) -> NotetypeDict | None:
method all (line 173) | def all(self) -> list[NotetypeDict]:
method by_name (line 177) | def by_name(self, name: str) -> NotetypeDict | None:
method new (line 185) | def new(self, name: str) -> NotetypeDict:
method remove_all_notetypes (line 196) | def remove_all_notetypes(self) -> None:
method remove (line 201) | def remove(self, id: NotetypeId) -> OpChanges:
method add (line 206) | def add(self, notetype: NotetypeDict) -> OpChangesWithId:
method add_dict (line 214) | def add_dict(self, notetype: NotetypeDict) -> OpChangesWithId:
method ensure_name_unique (line 219) | def ensure_name_unique(self, notetype: NotetypeDict) -> None:
method update_dict (line 224) | def update_dict(
method _mutate_after_write (line 234) | def _mutate_after_write(self, notetype: NotetypeDict) -> None:
method nids (line 243) | def nids(self, ntid: NotetypeId) -> list[anki.notes.NoteId]:
method use_count (line 250) | def use_count(self, notetype: NotetypeDict) -> int:
method copy (line 259) | def copy(self, notetype: NotetypeDict, add: bool = True) -> NotetypeDict:
method field_map (line 274) | def field_map(self, notetype: NotetypeDict) -> dict[str, tuple[int, Fi...
method field_names (line 278) | def field_names(self, notetype: NotetypeDict) -> list[str]:
method sort_idx (line 281) | def sort_idx(self, notetype: NotetypeDict) -> int:
method cloze_fields (line 284) | def cloze_fields(self, mid: NotetypeId) -> Sequence[int]:
method new_field (line 291) | def new_field(self, name: str) -> FieldDict:
method add_field (line 301) | def add_field(self, notetype: NotetypeDict, field: FieldDict) -> None:
method remove_field (line 305) | def remove_field(self, notetype: NotetypeDict, field: FieldDict) -> None:
method reposition_field (line 309) | def reposition_field(
method rename_field (line 320) | def rename_field(
method set_sort_index (line 327) | def set_sort_index(self, notetype: NotetypeDict, idx: int) -> None:
method new_template (line 336) | def new_template(self, name: str) -> TemplateDict:
method add_template (line 347) | def add_template(self, notetype: NotetypeDict, template: TemplateDict)...
method remove_template (line 351) | def remove_template(self, notetype: NotetypeDict, template: TemplateDi...
method reposition_template (line 357) | def reposition_template(
method template_use_count (line 368) | def template_use_count(self, ntid: NotetypeId, ord: int) -> int:
method get_single_notetype_of_notes (line 380) | def get_single_notetype_of_notes(
method change_notetype_info (line 387) | def change_notetype_info(
method change_notetype_of_notes (line 394) | def change_notetype_of_notes(self, input: ChangeNotetypeRequest) -> Op...
method restore_notetype_to_stock (line 410) | def restore_notetype_to_stock(
method change (line 422) | def change(
method _convert_legacy_map (line 451) | def _convert_legacy_map(
method scmhash (line 469) | def scmhash(self, notetype: NotetypeDict) -> str:
method _availClozeOrds (line 482) | def _availClozeOrds(
method addTemplate (line 491) | def addTemplate(self, notetype: NotetypeDict, template: TemplateDict) ...
method remTemplate (line 497) | def remTemplate(self, notetype: NotetypeDict, template: TemplateDict) ...
method move_template (line 502) | def move_template(
method addField (line 509) | def addField(self, notetype: NotetypeDict, field: FieldDict) -> None:
method remField (line 515) | def remField(self, notetype: NotetypeDict, field: FieldDict) -> None:
method moveField (line 520) | def moveField(self, notetype: NotetypeDict, field: FieldDict, idx: int...
method renameField (line 525) | def renameField(
method rem (line 532) | def rem(self, m: NotetypeDict) -> None:
method set_current (line 537) | def set_current(self, m: NotetypeDict) -> None:
method all_names (line 541) | def all_names(self) -> list[str]:
method ids (line 545) | def ids(self) -> list[NotetypeId]:
method flush (line 549) | def flush(self) -> None:
method update (line 553) | def update(
method save (line 571) | def save(self, notetype: NotetypeDict | None = None, **legacy_kwargs: ...
FILE: pylib/anki/notes.py
class Note (line 29) | class Note(DeprecatedNamesMixin):
method __init__ (line 36) | def __init__(
method load (line 55) | def load(self) -> None:
method _load_from_backend_note (line 60) | def _load_from_backend_note(self, note: notes_pb2.Note) -> None:
method _to_backend_note (line 70) | def _to_backend_note(self) -> notes_pb2.Note:
method flush (line 83) | def flush(self) -> None:
method joined_fields (line 91) | def joined_fields(self) -> str:
method ephemeral_card (line 94) | def ephemeral_card(
method cards (line 134) | def cards(self) -> list[anki.cards.Card]:
method card_ids (line 137) | def card_ids(self) -> Sequence[anki.cards.CardId]:
method note_type (line 140) | def note_type(self) -> NotetypeDict | None:
method cloze_numbers_in_fields (line 145) | def cloze_numbers_in_fields(self) -> Sequence[int]:
method keys (line 151) | def keys(self) -> list[str]:
method values (line 154) | def values(self) -> list[str]:
method items (line 157) | def items(self) -> list[tuple[str, str]]:
method _field_index (line 160) | def _field_index(self, key: str) -> int:
method __getitem__ (line 166) | def __getitem__(self, key: str) -> str:
method __setitem__ (line 169) | def __setitem__(self, key: str, value: str) -> None:
method __contains__ (line 172) | def __contains__(self, key: str) -> bool:
method has_tag (line 178) | def has_tag(self, tag: str) -> bool:
method remove_tag (line 181) | def remove_tag(self, tag: str) -> None:
method add_tag (line 186) | def add_tag(self, tag: str) -> None:
method string_tags (line 190) | def string_tags(self) -> str:
method set_tags_from_str (line 193) | def set_tags_from_str(self, tags: str) -> None:
method fields_check (line 199) | def fields_check(self) -> NoteFieldsCheckResult.V:
FILE: pylib/anki/scheduler/base.py
class SchedulerBase (line 43) | class SchedulerBase(DeprecatedNamesMixin):
method __init__ (line 48) | def __init__(self, col: anki.collection.Collection) -> None:
method _timing_today (line 51) | def _timing_today(self) -> SchedTimingToday:
method today (line 55) | def today(self) -> int:
method day_cutoff (line 59) | def day_cutoff(self) -> int:
method countIdx (line 62) | def countIdx(self, card: Card) -> int:
method deck_due_tree (line 71) | def deck_due_tree(self, top_deck_id: None = None) -> DeckTreeNode: ...
method deck_due_tree (line 74) | def deck_due_tree(self, top_deck_id: DeckId) -> DeckTreeNode | None: ...
method deck_due_tree (line 76) | def deck_due_tree(self, top_deck_id: DeckId | None = None) -> DeckTree...
method congratulations_info (line 87) | def congratulations_info(self) -> CongratsInfo:
method have_buried_siblings (line 90) | def have_buried_siblings(self) -> bool:
method have_manually_buried (line 93) | def have_manually_buried(self) -> bool:
method have_buried (line 96) | def have_buried(self) -> bool:
method custom_study (line 100) | def custom_study(self, request: CustomStudyRequest) -> OpChanges:
method custom_study_defaults (line 103) | def custom_study_defaults(self, deck_id: DeckId) -> CustomStudyDefaults:
method extend_limits (line 106) | def extend_limits(self, new: int, rev: int) -> None:
method _deck_limit (line 112) | def _deck_limit(self) -> str:
method rebuild_filtered_deck (line 120) | def rebuild_filtered_deck(self, deck_id: DeckId) -> OpChangesWithCount:
method empty_filtered_deck (line 123) | def empty_filtered_deck(self, deck_id: DeckId) -> OpChanges:
method get_or_create_filtered_deck (line 126) | def get_or_create_filtered_deck(self, deck_id: DeckId) -> FilteredDeck...
method add_or_update_filtered_deck (line 129) | def add_or_update_filtered_deck(
method filtered_deck_order_labels (line 134) | def filtered_deck_order_labels(self) -> Sequence[str]:
method unsuspend_cards (line 140) | def unsuspend_cards(self, ids: Sequence[CardId]) -> OpChanges:
method unbury_cards (line 143) | def unbury_cards(self, ids: Sequence[CardId]) -> OpChanges:
method unbury_deck (line 146) | def unbury_deck(
method suspend_cards (line 153) | def suspend_cards(self, ids: Sequence[CardId]) -> OpChangesWithCount:
method suspend_notes (line 158) | def suspend_notes(self, ids: Sequence[NoteId]) -> OpChangesWithCount:
method bury_cards (line 163) | def bury_cards(
method bury_notes (line 174) | def bury_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount:
method schedule_cards_as_new (line 182) | def schedule_cards_as_new(
method schedule_cards_as_new_defaults (line 200) | def schedule_cards_as_new_defaults(
method set_due_date (line 205) | def set_due_date(
method reset_cards (line 226) | def reset_cards(self, ids: list[CardId]) -> None:
method reposition_new_cards (line 247) | def reposition_new_cards(
method reposition_defaults (line 263) | def reposition_defaults(self) -> RepositionDefaults:
method randomize_cards (line 266) | def randomize_cards(self, did: DeckId) -> None:
method order_cards (line 269) | def order_cards(self, did: DeckId) -> None:
method resort_conf (line 272) | def resort_conf(self, conf: DeckConfigDict) -> None:
method maybe_randomize_deck (line 280) | def maybe_randomize_deck(self, did: DeckId | None = None) -> None:
method _legacy_sort_cards (line 288) | def _legacy_sort_cards(
FILE: pylib/anki/scheduler/dummy.py
class DummyScheduler (line 12) | class DummyScheduler(SchedulerBaseWithLegacy):
method reset (line 15) | def reset(self) -> None:
method getCard (line 18) | def getCard(self) -> Card | None:
method answerCard (line 21) | def answerCard(self, card: Card, ease: int) -> None:
method _is_finished (line 24) | def _is_finished(self) -> bool:
method active_decks (line 28) | def active_decks(self) -> list[DeckId]:
method counts (line 31) | def counts(self) -> tuple[int, int, int]:
FILE: pylib/anki/scheduler/legacy.py
class SchedulerBaseWithLegacy (line 20) | class SchedulerBaseWithLegacy(SchedulerBase):
method reschedCards (line 23) | def reschedCards(
method buryNote (line 28) | def buryNote(self, nid: NoteId) -> None:
method unburyCards (line 32) | def unburyCards(self) -> None:
method unburyCardsForDeck (line 36) | def unburyCardsForDeck(self, type: str = "all") -> None:
method finishedMsg (line 46) | def finishedMsg(self) -> str:
method _nextDueMsg (line 50) | def _nextDueMsg(self) -> str:
method rebuildDyn (line 54) | def rebuildDyn(self, did: DeckId | None = None) -> int | None:
method emptyDyn (line 63) | def emptyDyn(self, did: DeckId | None, lim: str | None = None) -> None:
method remFromDyn (line 84) | def remFromDyn(self, cids: list[CardId]) -> None:
method update_stats (line 88) | def update_stats(
method _updateStats (line 102) | def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
method deckDueTree (line 111) | def deckDueTree(self) -> list:
method total_rev_for_current_deck (line 119) | def total_rev_for_current_deck(self) -> int:
method answerButtons (line 129) | def answerButtons(self, card: Card) -> int:
method _cardConf (line 134) | def _cardConf(self, card: Card) -> DeckConfigDict:
method _fuzzIvlRange (line 137) | def _fuzzIvlRange(self, ivl: int) -> tuple[int, int]:
FILE: pylib/anki/scheduler/v3.py
class Scheduler (line 39) | class Scheduler(SchedulerBaseWithLegacy):
method get_queued_cards (line 48) | def get_queued_cards(
method describe_next_states (line 59) | def describe_next_states(self, next_states: SchedulingStates) -> Seque...
method build_answer (line 66) | def build_answer(
method answer_card (line 94) | def answer_card(self, input: CardAnswer) -> OpChanges:
method state_is_leech (line 100) | def state_is_leech(self, new_state: SchedulingState) -> bool:
method reset (line 108) | def reset(self) -> None:
method getCard (line 112) | def getCard(self) -> Card | None:
method _is_finished (line 124) | def _is_finished(self) -> bool:
method counts (line 128) | def counts(self, card: Card | None = None) -> tuple[int, int, int]:
method newCount (line 133) | def newCount(self) -> int:
method lrnCount (line 137) | def lrnCount(self) -> int:
method reviewCount (line 141) | def reviewCount(self) -> int:
method nextIvlStr (line 144) | def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
method answerCard (line 152) | def answerCard(self, card: Card, ease: Literal[1, 2, 3, 4]) -> OpChanges:
method _interval_for_state (line 178) | def _interval_for_state(self, state: scheduler_pb2.SchedulingState) ->...
method _interval_for_normal_state (line 188) | def _interval_for_normal_state(
method _interval_for_filtered_state (line 204) | def _interval_for_filtered_state(
method nextIvl (line 216) | def nextIvl(self, card: Card, ease: int) -> Any:
method active_decks (line 237) | def active_decks(self) -> list[DeckId]:
FILE: pylib/anki/sound.py
class TTSTag (line 22) | class TTSTag:
class SoundOrVideoTag (line 37) | class SoundOrVideoTag:
method path (line 51) | def path(self, media_folder: str) -> str:
function strip_av_refs (line 72) | def strip_av_refs(text: str) -> str:
FILE: pylib/anki/stats.py
function _legacy_card_stats (line 25) | def _legacy_card_stats(
class CardStats (line 47) | class CardStats:
method __init__ (line 50) | def __init__(self, col: anki.collection.Collection, card: anki.cards.C...
method report (line 56) | def report(self, include_revlog: bool = False) -> str:
method addLine (line 61) | def addLine(self, k: str, v: int | str) -> None:
method makeLine (line 64) | def makeLine(self, k: str, v: str | int) -> str:
method date (line 69) | def date(self, tm: float) -> str:
method time (line 72) | def time(self, tm: float) -> str:
class CollectionStats (line 96) | class CollectionStats:
method __init__ (line 97) | def __init__(self, col: anki.collection.Collection) -> None:
method report (line 106) | def report(self, type: int = PERIOD_MONTH) -> str:
method _section (line 123) | def _section(self, txt: str) -> str:
method todayStats (line 141) | def todayStats(self) -> str:
method get_start_end_chunk (line 211) | def get_start_end_chunk(self, by: str = "review") -> tuple[int, int | ...
method dueGraph (line 227) | def dueGraph(self) -> str:
method _dueInfo (line 272) | def _dueInfo(self, tot: int, num: int) -> str:
method _due (line 291) | def _due(
method introductionGraph (line 316) | def introductionGraph(self) -> str:
method repsGraphs (line 350) | def repsGraphs(self) -> str:
method _ansInfo (line 408) | def _ansInfo(
method _splitRepData (line 456) | def _splitRepData(
method _added (line 497) | def _added(self, num: int | None = 7, chunk: int = 1) -> Any:
method _done (line 524) | def _done(self, num: int | None = 7, chunk: int = 1) -> Any:
method _daysStudied (line 568) | def _daysStudied(self) -> Any:
method ivlGraph (line 597) | def ivlGraph(self) -> str:
method _ivls (line 638) | def _ivls(self) -> tuple[list[Any], int]:
method easeGraph (line 667) | def easeGraph(self) -> str:
method _easeInfo (line 712) | def _easeInfo(self, eases: list[tuple[int, int, int]]) -> str:
method _eases (line 739) | def _eases(self) -> Any:
method hourGraph (line 769) | def hourGraph(self) -> str:
method _hourRet (line 836) | def _hourRet(self) -> Any:
method cardGraph (line 860) | def cardGraph(self) -> str:
method _line (line 900) | def _line(self, i: list[str], a: str, b: int | str, bold: bool = True)...
method _lineTbl (line 914) | def _lineTbl(self, i: list[str]) -> str:
method _factors (line 917) | def _factors(self) -> Any:
method _cards (line 928) | def _cards(self) -> Any:
method footer (line 943) | def footer(self) -> str:
method _graph (line 959) | def _graph(
method _limit (line 1074) | def _limit(self) -> Any:
method _revlogLimit (line 1079) | def _revlogLimit(self) -> str:
method _title (line 1086) | def _title(self, title: str, subtitle: str = "") -> str:
method _deckAge (line 1089) | def _deckAge(self, by: str) -> int:
method _periodDays (line 1108) | def _periodDays(self) -> int | None:
method _avgDay (line 1114) | def _avgDay(self, tot: float, num: int, unit: str) -> str:
FILE: pylib/anki/stdmodels.py
function _get_stock_notetype (line 22) | def _get_stock_notetype(
function get_stock_notetypes (line 28) | def get_stock_notetypes(
function _legacy_add_basic_model (line 66) | def _legacy_add_basic_model(
function _legacy_add_basic_typing_model (line 74) | def _legacy_add_basic_typing_model(
function _legacy_add_forward_reverse (line 82) | def _legacy_add_forward_reverse(
function _legacy_add_forward_optional_reverse (line 90) | def _legacy_add_forward_optional_reverse(
function _legacy_add_cloze_model (line 98) | def _legacy_add_cloze_model(
function __getattr__ (line 121) | def __getattr__(name: str) -> Any:
FILE: pylib/anki/sync.py
class Syncer (line 19) | class Syncer:
method sync (line 20) | def sync(self) -> str:
FILE: pylib/anki/syncserver.py
function run_sync_server (line 5) | def run_sync_server() -> None:
FILE: pylib/anki/tags.py
class TagManager (line 34) | class TagManager(DeprecatedNamesMixin):
method __init__ (line 35) | def __init__(self, col: anki.collection.Collection) -> None:
method all (line 39) | def all(self) -> list[str]:
method __repr__ (line 42) | def __repr__(self) -> str:
method tree (line 47) | def tree(self) -> TagTreeNode:
method clear_unused_tags (line 53) | def clear_unused_tags(self) -> OpChangesWithCount:
method set_collapsed (line 56) | def set_collapsed(self, tag: str, collapsed: bool) -> OpChanges:
method bulk_add (line 63) | def bulk_add(self, note_ids: Sequence[NoteId], tags: str) -> OpChanges...
method bulk_remove (line 67) | def bulk_remove(self, note_ids: Sequence[NoteId], tags: str) -> OpChan...
method find_and_replace (line 73) | def find_and_replace(
method rename (line 95) | def rename(self, old: str, new: str) -> OpChangesWithCount:
method remove (line 99) | def remove(self, space_separated_tags: str) -> OpChangesWithCount:
method reparent (line 103) | def reparent(self, tags: Sequence[str], new_parent: str) -> OpChangesW...
method split (line 111) | def split(self, tags: str) -> list[str]:
method join (line 115) | def join(self, tags: list[str]) -> str:
method rem_from_str (line 121) | def rem_from_str(self, deltags: str, tags: str) -> str:
method canonify (line 144) | def canonify(self, tag_list: list[str]) -> list[str]:
method in_list (line 147) | def in_list(self, tag: str, tags: list[str]) -> bool:
method _legacy_register_notes (line 154) | def _legacy_register_notes(self, nids: list[int] | None = None) -> None:
method register (line 157) | def register(
method _legacy_bulk_add (line 162) | def _legacy_bulk_add(self, ids: list[NoteId], tags: str, add: bool = T...
method _legacy_bulk_rem (line 169) | def _legacy_bulk_rem(self, ids: list[NoteId], tags: str) -> None:
method by_deck (line 173) | def by_deck(self, did: DeckId, children: bool = False) -> list[str]:
FILE: pylib/anki/template.py
class TemplateReplacement (line 49) | class TemplateReplacement:
class PartiallyRenderedCard (line 59) | class PartiallyRenderedCard:
method from_proto (line 67) | def from_proto(
method nodes_from_proto (line 78) | def nodes_from_proto(
function av_tag_to_native (line 96) | def av_tag_to_native(tag: card_rendering_pb2.AVTag) -> AVTag:
function av_tags_to_native (line 110) | def av_tags_to_native(tags: Sequence[card_rendering_pb2.AVTag]) -> list[...
class TemplateRenderContext (line 114) | class TemplateRenderContext:
method from_existing_card (line 121) | def from_existing_card(
method from_card_layout (line 127) | def from_card_layout(
method __init__ (line 144) | def __init__(
method question_side (line 173) | def question_side(self) -> bool:
method col (line 176) | def col(self) -> anki.collection.Collection:
method fields (line 179) | def fields(self) -> dict[str, str]:
method card (line 200) | def card(self) -> anki.cards.Card:
method note (line 207) | def note(self) -> anki.notes.Note:
method note_type (line 210) | def note_type(self) -> NotetypeDict:
method latex_svg (line 213) | def latex_svg(self) -> bool:
method qfmt (line 217) | def qfmt(self) -> str:
method afmt (line 221) | def afmt(self) -> str:
method render (line 224) | def render(self) -> TemplateRenderOutput:
method _partially_render (line 258) | def _partially_render(self) -> PartiallyRenderedCard:
class TemplateRenderOutput (line 280) | class TemplateRenderOutput:
method question_and_style (line 289) | def question_and_style(self) -> str:
method answer_and_style (line 292) | def answer_and_style(self) -> str:
function templates_for_card (line 297) | def templates_for_card(card: anki.cards.Card, browser: bool) -> tuple[st...
function apply_custom_filters (line 308) | def apply_custom_filters(
FILE: pylib/anki/types.py
function assert_exhaustive (line 7) | def assert_exhaustive(arg: NoReturn) -> NoReturn:
FILE: pylib/anki/utils.py
function to_json_bytes (line 34) | def to_json_bytes(obj: Any) -> bytes:
function int_time (line 44) | def int_time(scale: int = 1) -> int:
function strip_html (line 53) | def strip_html(txt: str) -> str:
function strip_html_media (line 60) | def strip_html_media(txt: str) -> str:
function html_to_text_line (line 70) | def html_to_text_line(txt: str) -> str:
function ids2str (line 82) | def ids2str(ids: Iterable[int | str]) -> str:
function timestamp_id (line 87) | def timestamp_id(db: DBProxy, table: str) -> int:
function max_id (line 97) | def max_id(db: DBProxy) -> int:
function base62 (line 106) | def base62(num: int, extra: str = "") -> str:
function base91 (line 118) | def base91(num: int) -> str:
function guid64 (line 123) | def guid64() -> str:
function join_fields (line 132) | def join_fields(list: list[str]) -> str:
function split_fields (line 136) | def split_fields(string: str) -> list[str]:
function checksum (line 144) | def checksum(data: bytes | str) -> str:
function field_checksum (line 150) | def field_checksum(data: str) -> int:
function tmpdir (line 161) | def tmpdir() -> str:
function tmpfile (line 181) | def tmpfile(prefix: str = "", suffix: str = "") -> str:
function namedtmp (line 187) | def namedtmp(name: str, remove: bool = True) -> str:
function no_bundled_libs (line 203) | def no_bundled_libs() -> Iterator[None]:
function call (line 210) | def call(argv: list[str], wait: bool = True, **kwargs: Any) -> int:
function invalid_filename (line 259) | def invalid_filename(str: str, dirsep: bool = True) -> str | None:
function plat_desc (line 272) | def plat_desc() -> str:
function version_with_build (line 300) | def version_with_build() -> str:
function int_version (line 306) | def int_version() -> int:
function int_version_to_str (line 330) | def int_version_to_str(ver: int) -> str:
function __getattr__ (line 361) | def __getattr__(name: str) -> Any:
FILE: pylib/hatch_build.py
class CustomBuildHook (line 13) | class CustomBuildHook(BuildHookInterface):
method initialize (line 18) | def initialize(self, version: str, build_data: Dict[str, Any]) -> None:
method _should_exclude (line 44) | def _should_exclude(self, path: Path) -> bool:
FILE: pylib/rsbridge/build.rs
function main (line 4) | fn main() {
FILE: pylib/rsbridge/lib.rs
type Backend (line 15) | struct Backend {
method command (line 49) | fn command(
method db_command (line 67) | fn db_command(&self, py: Python, input: &Bound<'_, PyBytes>) -> PyResu...
function buildhash (line 22) | fn buildhash() -> &'static str {
function initialize_logging (line 28) | fn initialize_logging(path: Option<&str>) -> PyResult<()> {
function syncserver (line 33) | fn syncserver() -> PyResult<()> {
function open_backend (line 40) | fn open_backend(init_msg: &Bound<'_, PyBytes>) -> PyResult<Backend> {
function _rsbridge (line 84) | fn _rsbridge(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
FILE: pylib/tests/shared.py
function adjusted_time (line 18) | def adjusted_time():
function assertException (line 26) | def assertException(exception, func):
function getEmptyCol (line 40) | def getEmptyCol():
function getEmptyDeckWith (line 56) | def getEmptyDeckWith(**kwargs):
function getUpgradeDeckPath (line 63) | def getUpgradeDeckPath(name="anki12.anki"):
function errorsAfterMidnight (line 73) | def errorsAfterMidnight(func):
function isNearCutoff (line 84) | def isNearCutoff():
FILE: pylib/tests/test_cards.py
function test_delete (line 9) | def test_delete():
function test_misc (line 25) | def test_misc():
function test_genrem (line 36) | def test_genrem():
function test_gendeck (line 68) | def test_gendeck():
FILE: pylib/tests/test_collection.py
function test_create_open (line 18) | def test_create_open():
function test_noteAddDelete (line 50) | def test_noteAddDelete():
function test_fieldChecksum (line 89) | def test_fieldChecksum():
function test_addDelTags (line 102) | def test_addDelTags():
function test_timestamps (line 123) | def test_timestamps():
function test_furigana (line 131) | def test_furigana():
function test_translate (line 153) | def test_translate():
function test_db_named_args (line 165) | def test_db_named_args(capsys):
FILE: pylib/tests/test_decks.py
function test_basic (line 10) | def test_basic():
function test_remove (line 45) | def test_remove():
function test_rename (line 64) | def test_rename():
FILE: pylib/tests/test_exporting.py
function getEmptyCol (line 16) | def getEmptyCol():
function setup1 (line 26) | def setup1():
function test_export_anki (line 47) | def test_export_anki():
function test_export_ankipkg (line 89) | def test_export_ankipkg():
function test_export_anki_due (line 106) | def test_export_anki_due():
function test_export_textnote (line 147) | def test_export_textnote():
function test_exporters (line 164) | def test_exporters():
FILE: pylib/tests/test_find.py
class DummyCollection (line 12) | class DummyCollection:
method weakref (line 13) | def weakref(self):
function test_find_cards (line 17) | def test_find_cards():
function test_findReplace (line 235) | def test_findReplace():
function test_findDupes (line 285) | def test_findDupes():
FILE: pylib/tests/test_flags.py
function test_flags (line 7) | def test_flags():
FILE: pylib/tests/test_importing.py
function clear_tempfile (line 23) | def clear_tempfile(tf):
function test_anki2_mediadupes (line 32) | def test_anki2_mediadupes():
function test_apkg (line 78) | def test_apkg():
function test_anki2_diffmodel_templates (line 99) | def test_anki2_diffmodel_templates():
function test_anki2_updates (line 121) | def test_anki2_updates():
function test_csv (line 149) | def test_csv():
function test_csv2 (line 184) | def test_csv2():
function test_tsv_tag_modified (line 208) | def test_tsv_tag_modified():
function test_tsv_tag_multiple_tags (line 244) | def test_tsv_tag_multiple_tags():
function test_csv_tag_only_if_modified (line 278) | def test_csv_tag_only_if_modified():
function test_mnemo (line 308) | def test_mnemo():
FILE: pylib/tests/test_latex.py
function test_latex (line 14) | def test_latex():
FILE: pylib/tests/test_media.py
function test_add (line 13) | def test_add():
function test_strings (line 31) | def test_strings():
function test_deckIntegration (line 60) | def test_deckIntegration():
FILE: pylib/tests/test_models.py
function encode_attribute (line 15) | def encode_attribute(s):
function test_modelDelete (line 21) | def test_modelDelete():
function test_modelCopy (line 32) | def test_modelCopy():
function test_fields (line 46) | def test_fields():
function test_templates (line 93) | def test_templates():
function test_cloze_ordinals (line 140) | def test_cloze_ordinals():
function test_text (line 164) | def test_text():
function test_cloze (line 175) | def test_cloze():
function test_cloze_mathjax (line 247) | def test_cloze_mathjax():
function test_typecloze (line 301) | def test_typecloze():
function test_chained_mods (line 312) | def test_chained_mods():
function test_modelChange (line 351) | def test_modelChange():
function test_req (line 434) | def test_req():
FILE: pylib/tests/test_schedv3.py
function getEmptyCol (line 20) | def getEmptyCol():
function test_clock (line 25) | def test_clock():
function test_basics (line 31) | def test_basics():
function test_new (line 36) | def test_new():
function test_newLimits (line 80) | def test_newLimits():
function test_newBoxes (line 112) | def test_newBoxes():
function test_learn (line 128) | def test_learn():
function test_relearn (line 187) | def test_relearn():
function test_relearn_no_steps (line 213) | def test_relearn_no_steps():
function test_learn_collapsed (line 235) | def test_learn_collapsed():
function test_learn_day (line 261) | def test_learn_day():
function test_reviews (line 324) | def test_reviews():
function review_limits_setup (line 391) | def review_limits_setup() -> tuple[anki.collection.Collection, dict]:
function test_review_limits (line 428) | def test_review_limits():
function test_button_spacing (line 451) | def test_button_spacing():
function test_nextIvl (line 478) | def test_nextIvl():
function test_bury (line 543) | def test_bury():
function test_suspend (line 581) | def test_suspend():
function test_filt_reviewing_early_normal (line 627) | def test_filt_reviewing_early_normal():
function test_filt_keep_lrn_state (line 678) | def test_filt_keep_lrn_state():
function test_preview (line 721) | def test_preview():
function test_ordcycle (line 768) | def test_ordcycle():
function test_counts_idx_new (line 800) | def test_counts_idx_new():
function test_repCounts (line 825) | def test_repCounts():
function test_timing (line 878) | def test_timing():
function test_collapse (line 903) | def test_collapse():
function test_deckDue (line 930) | def test_deckDue():
function test_deckTree (line 981) | def test_deckTree():
function test_deckFlow (line 991) | def test_deckFlow():
function test_reorder (line 1019) | def test_reorder():
function test_forget (line 1063) | def test_forget():
function test_resched (line 1079) | def test_resched():
function test_norelearn (line 1097) | def test_norelearn():
function test_failmult (line 1118) | def test_failmult():
function test_negativeDueFilter (line 1146) | def test_negativeDueFilter():
function test_initial_repeat (line 1171) | def test_initial_repeat():
FILE: pylib/tests/test_stats.py
function test_stats (line 11) | def test_stats():
function test_graphs_empty (line 27) | def test_graphs_empty():
function test_graphs (line 32) | def test_graphs():
FILE: pylib/tests/test_template.py
function test_deferred_frontside (line 7) | def test_deferred_frontside():
FILE: pylib/tests/test_utils.py
function test_int_version_to_str (line 7) | def test_int_version_to_str():
FILE: pylib/tools/hookslib.py
class Hook (line 21) | class Hook:
method callable (line 40) | def callable(self) -> str:
method arg_names (line 50) | def arg_names(self, args: list[str] | None) -> list[str]:
method full_name (line 59) | def full_name(self) -> str:
method kind (line 62) | def kind(self) -> str:
method classname (line 68) | def classname(self) -> str:
method list_code (line 71) | def list_code(self) -> str:
method code (line 76) | def code(self) -> str:
method fire_code (line 102) | def fire_code(self) -> str:
method legacy_args (line 110) | def legacy_args(self) -> str:
method replaced_args (line 117) | def replaced_args(self) -> str:
method hook_fire_code (line 121) | def hook_fire_code(self) -> str:
method filter_fire_code (line 157) | def filter_fire_code(self) -> str:
function write_file (line 198) | def write_file(path: str, hooks: list[Hook], prefix: str, suffix: str):
FILE: qt/aqt/__init__.py
class DialogManager (line 125) | class DialogManager:
method open (line 139) | def open(self, name: str, *args: Any, **kwargs: Any) -> Any:
method markClosed (line 156) | def markClosed(self, name: str) -> None:
method allClosed (line 159) | def allClosed(self) -> bool:
method closeAll (line 162) | def closeAll(self, onsuccess: Callable[[], None]) -> bool | None:
method register_dialog (line 188) | def register_dialog(
function setupLangAndBackend (line 230) | def setupLangAndBackend(
class NativeEventFilter (line 290) | class NativeEventFilter(QAbstractNativeEventFilter):
method nativeEventFilter (line 291) | def nativeEventFilter(
class AnkiApp (line 307) | class AnkiApp(QApplication):
method __init__ (line 316) | def __init__(self, argv: list[str]) -> None:
method _set_windows_shutdown_block_reason (line 324) | def _set_windows_shutdown_block_reason(self, reason: str) -> None:
method _unset_windows_shutdown_block_reason (line 335) | def _unset_windows_shutdown_block_reason(self) -> None:
method secondInstance (line 344) | def secondInstance(self) -> bool:
method sendMsg (line 363) | def sendMsg(self, txt: str) -> bool:
method onRecv (line 382) | def onRecv(self) -> None:
method event (line 396) | def event(self, evt: QEvent | None) -> bool:
method eventFilter (line 407) | def eventFilter(self, src: Any, evt: QEvent | None) -> bool:
function parseArgs (line 453) | def parseArgs(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
function setupGL (line 478) | def setupGL(pm: aqt.profiles.ProfileManager) -> None:
function write_profile_results (line 570) | def write_profile_results() -> None:
function run (line 578) | def run() -> None:
function _run (line 591) | def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | ...
FILE: qt/aqt/about.py
class ClosableQDialog (line 15) | class ClosableQDialog(QDialog):
method reject (line 16) | def reject(self) -> None:
method accept (line 20) | def accept(self) -> None:
method closeWithCallback (line 24) | def closeWithCallback(self, callback: Callable[[], None]) -> None:
function show (line 29) | def show(mw: aqt.AnkiQt) -> QDialog:
FILE: qt/aqt/addcards.py
class AddCards (line 37) | class AddCards(QMainWindow):
method __init__ (line 38) | def __init__(self, mw: AnkiQt) -> None:
method set_deck (line 62) | def set_deck(self, deck_id: DeckId) -> None:
method set_note_type (line 65) | def set_note_type(self, note_type_id: NotetypeId) -> None:
method set_note (line 68) | def set_note(self, note: Note, deck_id: DeckId | None = None) -> None:
method setupEditor (line 83) | def setupEditor(self) -> None:
method setup_choosers (line 91) | def setup_choosers(self) -> None:
method reopen (line 110) | def reopen(self, mw: AnkiQt) -> None:
method helpRequested (line 120) | def helpRequested(self) -> None:
method setupButtons (line 123) | def setupButtons(self) -> None:
method setAndFocusNote (line 156) | def setAndFocusNote(self, note: Note) -> None:
method show_notetype_selector (line 159) | def show_notetype_selector(self) -> None:
method on_deck_changed (line 162) | def on_deck_changed(self, deck_id: int) -> None:
method on_notetype_change (line 165) | def on_notetype_change(
method _load_new_note (line 218) | def _load_new_note(self, sticky_fields_from: Note | None = None) -> None:
method on_operation_did_execute (line 231) | def on_operation_did_execute(
method _new_note (line 244) | def _new_note(self) -> Note:
method addHistory (line 249) | def addHistory(self, note: Note) -> None:
method onHistory (line 254) | def onHistory(self) -> None:
method editHistory (line 276) | def editHistory(self, nid: NoteId) -> None:
method add_current_note (line 279) | def add_current_note(self) -> None:
method _add_current_note (line 287) | def _add_current_note(self) -> None:
method _note_can_be_added (line 314) | def _note_can_be_added(self, note: Note) -> bool:
method keyPressEvent (line 344) | def keyPressEvent(self, evt: QKeyEvent) -> None:
method closeEvent (line 350) | def closeEvent(self, evt: QCloseEvent) -> None:
method _close (line 357) | def _close(self) -> None:
method ifCanClose (line 369) | def ifCanClose(self, onOk: Callable) -> None:
method closeWithCallback (line 389) | def closeWithCallback(self, cb: Callable[[], None]) -> None:
method deckChooser (line 399) | def deckChooser(self) -> DeckChooser:
method addNote (line 411) | def addNote(self, note: Note) -> None:
method removeTempNote (line 415) | def removeTempNote(self, note: Note) -> None:
FILE: qt/aqt/addons.py
class AbortAddonImport (line 63) | class AbortAddonImport(Exception):
class InstallOk (line 69) | class InstallOk:
class InstallError (line 76) | class InstallError:
class DownloadOk (line 81) | class DownloadOk:
class DownloadError (line 91) | class DownloadError:
class AddonMeta (line 108) | class AddonMeta:
method human_name (line 121) | def human_name(self) -> str:
method ankiweb_id (line 124) | def ankiweb_id(self) -> int | None:
method compatible (line 131) | def compatible(self) -> bool:
method is_latest (line 140) | def is_latest(self, server_update_time: int) -> bool:
method page (line 143) | def page(self) -> str | None:
method from_json_meta (line 149) | def from_json_meta(dir_name: str, json_meta: dict[str, Any]) -> AddonM...
function package_name_valid (line 165) | def package_name_valid(name: str) -> bool:
class AddonManager (line 179) | class AddonManager:
method __init__ (line 208) | def __init__(self, mw: aqt.main.AnkiQt) -> None:
method allAddons (line 216) | def allAddons(self) -> list[str]:
method all_addon_meta (line 228) | def all_addon_meta(self) -> Iterable[AddonMeta]:
method addonsFolder (line 231) | def addonsFolder(self, module: str | None = None) -> str:
method loadAddons (line 237) | def loadAddons(self) -> None:
method onAddonsDialog (line 304) | def onAddonsDialog(self) -> None:
method addon_meta (line 310) | def addon_meta(self, dir_name: str) -> AddonMeta:
method write_addon_meta (line 315) | def write_addon_meta(self, addon: AddonMeta) -> None:
method _addonMetaPath (line 333) | def _addonMetaPath(self, module: str) -> str:
method addonMeta (line 337) | def addonMeta(self, module: str) -> dict[str, Any]:
method writeAddonMeta (line 350) | def writeAddonMeta(self, module: str, meta: dict[str, Any]) -> None:
method toggleEnabled (line 355) | def toggleEnabled(self, module: str, enable: bool | None = None) -> None:
method ankiweb_addons (line 373) | def ankiweb_addons(self) -> list[int]:
method isEnabled (line 383) | def isEnabled(self, module: str) -> bool:
method addonName (line 386) | def addonName(self, module: str) -> str:
method addonConflicts (line 389) | def addonConflicts(self, module: str) -> list[str]:
method annotatedName (line 392) | def annotatedName(self, module: str) -> str:
method allAddonConflicts (line 402) | def allAddonConflicts(self) -> dict[str, list[str]]:
method _disableConflicting (line 411) | def _disableConflicting(
method readManifestFile (line 432) | def readManifestFile(self, zfile: ZipFile) -> dict[Any, Any]:
method install (line 445) | def install(
method _install (line 493) | def _install(self, module: str, zfile: ZipFile) -> None:
method deleteAddon (line 518) | def deleteAddon(self, module: str) -> None:
method processPackages (line 524) | def processPackages(
method _installationErrorReport (line 555) | def _installationErrorReport(
method _installationSuccessReport (line 572) | def _installationSuccessReport(
method update_supported_versions (line 598) | def update_supported_versions(self, items: list[AddonInfo]) -> None:
method get_updated_addons (line 622) | def get_updated_addons(self, items: list[AddonInfo]) -> list[AddonInfo]:
method addonConfigDefaults (line 645) | def addonConfigDefaults(self, module: str) -> dict[str, Any] | None:
method set_config_help_action (line 653) | def set_config_help_action(self, module: str, action: Callable[[], str...
method addonConfigHelp (line 658) | def addonConfigHelp(self, module: str) -> str:
method addonFromModule (line 671) | def addonFromModule(self, module: str) -> str: # softly deprecated
method addon_from_module (line 675) | def addon_from_module(module: str) -> str:
method configAction (line 678) | def configAction(self, module: str) -> Callable[[], bool | None]:
method configUpdatedAction (line 681) | def configUpdatedAction(self, module: str) -> Callable[[Any], None]:
method _addon_schema_path (line 687) | def _addon_schema_path(self, module: str) -> str:
method _addon_schema (line 690) | def _addon_schema(self, module: str) -> Any:
method getConfig (line 705) | def getConfig(self, module: str) -> dict[str, Any] | None:
method setConfigAction (line 717) | def setConfigAction(self, module: str, fn: Callable[[], bool | None]) ...
method setConfigUpdatedAction (line 721) | def setConfigUpdatedAction(self, module: str, fn: Callable[[Any], None...
method writeConfig (line 725) | def writeConfig(self, module: str, conf: dict) -> None:
method _userFilesPath (line 734) | def _userFilesPath(self, sid: str) -> str:
method _userFilesBackupPath (line 737) | def _userFilesBackupPath(self) -> str:
method backupUserFiles (line 740) | def backupUserFiles(self, module: str) -> None:
method restoreUserFiles (line 746) | def restoreUserFiles(self, sid: str) -> None:
method setWebExports (line 759) | def setWebExports(self, module: str, pattern: str) -> None:
method getWebExports (line 763) | def getWebExports(self, module: str) -> str:
method get_logger (line 770) | def get_logger(cls, module: str) -> logging.Logger:
method has_logger (line 780) | def has_logger(self, module: str) -> bool:
method is_debug_logging_enabled (line 783) | def is_debug_logging_enabled(self, module: str) -> bool:
method toggle_debug_logging (line 788) | def toggle_debug_logging(self, module: str, enable: bool) -> None:
method logs_folder (line 793) | def logs_folder(self, module: str) -> Path:
class AddonsDialog (line 803) | class AddonsDialog(QDialog):
method __init__ (line 804) | def __init__(self, addonsManager: AddonManager) -> None:
method dragEnterEvent (line 835) | def dragEnterEvent(self, event: QDragEnterEvent) -> None:
method dropEvent (line 844) | def dropEvent(self, event: QDropEvent) -> None:
method reject (line 853) | def reject(self) -> None:
method name_for_addon_list (line 863) | def name_for_addon_list(self, addon: AddonMeta) -> str:
method compatible_string (line 873) | def compatible_string(self, addon: AddonMeta) -> str:
method should_grey (line 883) | def should_grey(self, addon: AddonMeta) -> bool:
method redrawAddons (line 886) | def redrawAddons(
method _onAddonSelectionChanged (line 908) | def _onAddonSelectionChanged(self) -> None:
method _onAddonItemSelected (line 929) | def _onAddonItemSelected(self, row_int: int) -> None:
method selectedAddons (line 937) | def selectedAddons(self) -> list[str]:
method onlyOneSelected (line 941) | def onlyOneSelected(self) -> str | None:
method selected_addon_meta (line 948) | def selected_addon_meta(self) -> AddonMeta | None:
method onToggleEnabled (line 955) | def onToggleEnabled(self) -> None:
method onViewPage (line 961) | def onViewPage(self) -> None:
method onViewFiles (line 968) | def onViewFiles(self) -> None:
method onDelete (line 982) | def onDelete(self) -> None:
method onGetAddons (line 1003) | def onGetAddons(self) -> None:
method after_downloading (line 1010) | def after_downloading(self, log: list[DownloadLogEntry]) -> None:
method onInstallFiles (line 1017) | def onInstallFiles(self, paths: list[str] | None = None) -> bool | None:
method check_for_updates (line 1034) | def check_for_updates(self) -> None:
method onConfig (line 1038) | def onConfig(self) -> None:
class GetAddons (line 1062) | class GetAddons(QDialog):
method __init__ (line 1063) | def __init__(self, dlg: AddonsDialog) -> None:
method onBrowse (line 1080) | def onBrowse(self) -> None:
method accept (line 1083) | def accept(self) -> None:
function download_addon (line 1104) | def download_addon(client: HttpClient, id: int) -> DownloadOk | Download...
class ExtractedDownloadMeta (line 1134) | class ExtractedDownloadMeta:
function extract_meta_from_download_url (line 1141) | def extract_meta_from_download_url(url: str) -> ExtractedDownloadMeta:
function download_log_to_html (line 1158) | def download_log_to_html(log: list[DownloadLogEntry]) -> str:
function describe_log_entry (line 1162) | def describe_log_entry(id_and_entry: DownloadLogEntry) -> str:
function download_encountered_problem (line 1186) | def download_encountered_problem(log: list[DownloadLogEntry]) -> bool:
function download_and_install_addon (line 1190) | def download_and_install_addon(
class DownloaderInstaller (line 1219) | class DownloaderInstaller(QObject):
method __init__ (line 1222) | def __init__(self, parent: QWidget, mgr: AddonManager, client: HttpCli...
method download (line 1233) | def download(
method _progress_callback (line 1254) | def _progress_callback(self, up: int, down: int) -> None:
method _download_all (line 1264) | def _download_all(self, force_enable: bool = False) -> None:
method _download_done (line 1272) | def _download_done(self, future: Future) -> None:
function show_log_to_user (line 1280) | def show_log_to_user(
function download_addons (line 1297) | def download_addons(
class ChooseAddonsToUpdateList (line 1315) | class ChooseAddonsToUpdateList(QListWidget):
method __init__ (line 1318) | def __init__(
method setup (line 1335) | def setup(self) -> None:
method bool_to_check (line 1360) | def bool_to_check(self, check_bool: bool) -> Qt.CheckState:
method checked (line 1366) | def checked(self, item: QListWidgetItem) -> bool:
method on_click (line 1369) | def on_click(self, item: QListWidgetItem) -> None:
method on_check (line 1376) | def on_check(self, item: QListWidgetItem) -> None:
method on_double_click (line 1382) | def on_double_click(self, item: QListWidgetItem) -> None:
method on_context_menu (line 1388) | def on_context_menu(self, point: QPoint) -> None:
method check_item (line 1397) | def check_item(self, item: QListWidgetItem, check: Qt.CheckState) -> N...
method header_checked (line 1403) | def header_checked(self, check: Qt.CheckState) -> None:
method refresh_header_check_state (line 1407) | def refresh_header_check_state(self) -> None:
method get_selected_addon_ids (line 1415) | def get_selected_addon_ids(self) -> list[int]:
method save_check_state (line 1424) | def save_check_state(self) -> None:
class ChooseAddonsToUpdateDialog (line 1433) | class ChooseAddonsToUpdateDialog(QDialog):
method __init__ (line 1436) | def __init__(
method setup (line 1447) | def setup(self) -> None:
method ask (line 1470) | def ask(self, on_done: Callable[[list[int]], None]) -> None:
method accept (line 1474) | def accept(self) -> None:
function fetch_update_info (line 1481) | def fetch_update_info(ids: list[int]) -> list[AddonInfo]:
function _fetch_update_info_batch (line 1496) | def _fetch_update_info_batch(chunk: Iterable[int]) -> Sequence[AddonInfo]:
function check_and_prompt_for_updates (line 1502) | def check_and_prompt_for_updates(
function check_for_updates (line 1514) | def check_for_updates(
function handle_update_info (line 1542) | def handle_update_info(
function prompt_to_update (line 1559) | def prompt_to_update(
function install_or_update_addon (line 1582) | def install_or_update_addon(
class ConfigEditor (line 1612) | class ConfigEditor(QDialog):
method __init__ (line 1613) | def __init__(self, dlg: AddonsDialog, addon: str, conf: dict) -> None:
method onRestoreDefaults (line 1642) | def onRestoreDefaults(self) -> None:
method setupFonts (line 1647) | def setupFonts(self) -> None:
method updateHelp (line 1654) | def updateHelp(self) -> None:
method updateText (line 1661) | def updateText(self, conf: dict[str, Any]) -> None:
method onClose (line 1674) | def onClose(self) -> None:
method reject (line 1678) | def reject(self) -> None:
method accept (line 1682) | def accept(self) -> None:
function installAddonPackages (line 1734) | def installAddonPackages(
FILE: qt/aqt/ankihub.py
function ankihub_login (line 33) | def ankihub_login(
function ankihub_logout (line 70) | def ankihub_logout(
function get_id_and_pass_from_user (line 88) | def get_id_and_pass_from_user(
function install_ankihub_addon (line 150) | def install_ankihub_addon(parent: QWidget, mgr: AddonManager) -> None:
FILE: qt/aqt/browser/browser.py
class MockModel (line 93) | class MockModel:
method __init__ (line 96) | def __init__(self, browser: aqt.browser.Browser) -> None:
method beginReset (line 100) | def beginReset(self) -> None:
method endReset (line 104) | def endReset(self) -> None:
method reset (line 108) | def reset(self) -> None:
class Browser (line 113) | class Browser(QMainWindow):
method __init__ (line 119) | def __init__(
method on_operation_did_execute (line 183) | def on_operation_did_execute(
method on_focus_change (line 214) | def on_focus_change(self, new: QWidget | None, old: QWidget | None) ->...
method set_layout (line 220) | def set_layout(self, mode: BrowserLayout, init: bool = False) -> None:
method maybe_update_layout (line 249) | def maybe_update_layout(self, aspect_ratio: float, force: bool = False...
method resizeEvent (line 256) | def resizeEvent(self, event: QResizeEvent | None) -> None:
method get_active_note_type_id (line 269) | def get_active_note_type_id(self) -> NotetypeId | None:
method add_card (line 279) | def add_card(self, deck_id: DeckId):
method _handle_close (line 289) | def _handle_close(self):
method setupMenus (line 299) | def setupMenus(self) -> None:
method _editor_web_view (line 405) | def _editor_web_view(self) -> EditorWebView:
method closeEvent (line 411) | def closeEvent(self, evt: QCloseEvent | None) -> None:
method _closeWindow (line 423) | def _closeWindow(self) -> None:
method closeWithCallback (line 442) | def closeWithCallback(self, onsuccess: Callable) -> None:
method keyPressEvent (line 446) | def keyPressEvent(self, evt: QKeyEvent | None) -> None:
method reopen (line 454) | def reopen(
method setupSearch (line 473) | def setupSearch(
method onSearchActivated (line 498) | def onSearchActivated(self) -> None:
method search_for (line 510) | def search_for(self, search: str, prompt: str | None = None) -> None:
method current_search (line 522) | def current_search(self) -> str:
method search (line 525) | def search(self) -> None:
method update_history (line 533) | def update_history(self) -> None:
method updateTitle (line 545) | def updateTitle(self) -> None:
method search_for_terms (line 557) | def search_for_terms(self, *search_terms: str | SearchNode) -> None:
method _default_search (line 562) | def _default_search(self, card: Card | None = None) -> None:
method onReset (line 574) | def onReset(self) -> None:
method begin_reset (line 580) | def begin_reset(self) -> None:
method end_reset (line 587) | def end_reset(self) -> None:
method setup_table (line 594) | def setup_table(self) -> None:
method setupEditor (line 604) | def setupEditor(self) -> None:
method on_all_or_selected_rows_changed (line 620) | def on_all_or_selected_rows_changed(self) -> None:
method onRowChanged (line 654) | def onRowChanged(self, *args: Any) -> None:
method on_current_row_changed (line 657) | def on_current_row_changed(self) -> None:
method _update_row_actions (line 665) | def _update_row_actions(self) -> None:
method _update_selection_actions (line 672) | def _update_selection_actions(self) -> None:
method _update_current_actions (line 689) | def _update_current_actions(self) -> None:
method on_table_state_changed (line 700) | def on_table_state_changed(self, checked: bool) -> None:
method setupSidebar (line 716) | def setupSidebar(self) -> None:
method showSidebar (line 753) | def showSidebar(self, show: bool = True) -> None:
method onSidebarVisibilityChange (line 756) | def onSidebarVisibilityChange(self, visible):
method focusSidebar (line 767) | def focusSidebar(self) -> None:
method focusSidebarSearchBar (line 771) | def focusSidebarSearchBar(self) -> None:
method toggle_sidebar (line 775) | def toggle_sidebar(self) -> None:
method setFilter (line 780) | def setFilter(self, *terms: str) -> None:
method showCardInfo (line 786) | def showCardInfo(self) -> None:
method _update_card_info (line 789) | def _update_card_info(self) -> None:
method selected_cards (line 795) | def selected_cards(self) -> Sequence[CardId]:
method selected_notes (line 798) | def selected_notes(self) -> Sequence[NoteId]:
method selectedNotesAsCards (line 801) | def selectedNotesAsCards(self) -> Sequence[CardId]:
method onHelp (line 804) | def onHelp(self) -> None:
method on_create_copy (line 815) | def on_create_copy(self) -> None:
method onChangeModel (line 826) | def onChangeModel(self) -> None:
method createFilteredDeck (line 830) | def createFilteredDeck(self) -> None:
method onTogglePreview (line 840) | def onTogglePreview(self) -> None:
method _renderPreview (line 850) | def _renderPreview(self) -> None:
method toggle_preview_button_state (line 857) | def toggle_preview_button_state(self, active: bool) -> None:
method _cleanup_preview (line 863) | def _cleanup_preview(self) -> None:
method _on_preview_closed (line 868) | def _on_preview_closed(self) -> None:
method delete_selected_notes (line 878) | def delete_selected_notes(self) -> None:
method set_deck_of_selected_cards (line 901) | def set_deck_of_selected_cards(self) -> None:
method add_tags_to_selected_notes (line 944) | def add_tags_to_selected_notes(
method remove_tags_from_selected_notes (line 962) | def remove_tags_from_selected_notes(self, tags: str | None = None) -> ...
method _prompt_for_tags (line 973) | def _prompt_for_tags(self, prompt: str) -> str | None:
method clear_unused_tags (line 982) | def clear_unused_tags(self) -> None:
method _update_toggle_suspend_action (line 992) | def _update_toggle_suspend_action(self) -> None:
method suspend_selected_cards (line 1000) | def suspend_selected_cards(self, checked: bool) -> None:
method _update_toggle_bury_action (line 1010) | def _update_toggle_bury_action(self) -> None:
method bury_selected_cards (line 1020) | def bury_selected_cards(self, checked: bool) -> None:
method _on_export_notes (line 1032) | def _on_export_notes(self) -> None:
method set_flag_of_selected_cards (line 1045) | def set_flag_of_selected_cards(self, flag: int) -> None:
method _update_flags_menu (line 1057) | def _update_flags_menu(self) -> None:
method _update_flag_labels (line 1066) | def _update_flag_labels(self) -> None:
method toggle_mark_of_selected_notes (line 1070) | def toggle_mark_of_selected_notes(self, checked: bool) -> None:
method _update_toggle_mark_action (line 1076) | def _update_toggle_mark_action(self) -> None:
method reposition (line 1088) | def reposition(self) -> None:
method set_due_date (line 1097) | def set_due_date(self) -> None:
method forget_cards (line 1108) | def forget_cards(self) -> None:
method grade_now (line 1119) | def grade_now(self) -> None:
method selectNotes (line 1162) | def selectNotes(self) -> None:
method setupHooks (line 1175) | def setupHooks(self) -> None:
method teardownHooks (line 1184) | def teardownHooks(self) -> None:
method _on_temporary_close (line 1193) | def _on_temporary_close(self, col: Collection) -> None:
method undo (line 1200) | def undo(self) -> None:
method redo (line 1203) | def redo(self) -> None:
method on_undo_state_change (line 1206) | def on_undo_state_change(self, info: UndoActionsInfo) -> None:
method onFindReplace (line 1218) | def onFindReplace(self) -> None:
method onFindDupes (line 1226) | def onFindDupes(self) -> None:
method has_previous_card (line 1234) | def has_previous_card(self) -> bool:
method has_next_card (line 1237) | def has_next_card(self) -> bool:
method onPreviousCard (line 1240) | def onPreviousCard(self) -> None:
method onNextCard (line 1246) | def onNextCard(self) -> None:
method onFirstCard (line 1252) | def onFirstCard(self) -> None:
method onLastCard (line 1255) | def onLastCard(self) -> None:
method onFind (line 1258) | def onFind(self) -> None:
method onNote (line 1262) | def onNote(self) -> None:
method onCardList (line 1271) | def onCardList(self) -> None:
method _line_edit (line 1274) | def _line_edit(self) -> QLineEdit:
FILE: qt/aqt/browser/card_info.py
class CardInfoDialog (line 28) | class CardInfoDialog(QDialog):
method __init__ (line 33) | def __init__(
method _setup_ui (line 51) | def _setup_ui(self, card_id: CardId | None) -> None:
method copy_card_info (line 77) | def copy_card_info(self, card_id: CardId | None) -> None:
method update_card (line 125) | def update_card(self, card_id: CardId | None) -> None:
method reject (line 134) | def reject(self) -> None:
class CardInfoManager (line 144) | class CardInfoManager:
method __init__ (line 147) | def __init__(self, mw: aqt.AnkiQt, geometry_key: str, window_title: str):
method show (line 154) | def show(self) -> None:
method set_card (line 168) | def set_card(self, card: Card | None) -> None:
method close (line 173) | def close(self) -> None:
method _on_close (line 177) | def _on_close(self) -> None:
class BrowserCardInfo (line 181) | class BrowserCardInfo(CardInfoManager):
method __init__ (line 182) | def __init__(self, mw: aqt.AnkiQt):
class ReviewerCardInfo (line 192) | class ReviewerCardInfo(CardInfoManager):
method __init__ (line 193) | def __init__(self, mw: aqt.AnkiQt):
class PreviousReviewerCardInfo (line 203) | class PreviousReviewerCardInfo(CardInfoManager):
method __init__ (line 204) | def __init__(self, mw: aqt.AnkiQt):
FILE: qt/aqt/browser/find_and_replace.py
class FindAndReplaceDialog (line 35) | class FindAndReplaceDialog(QDialog):
method __init__ (line 38) | def __init__(
method _show (line 68) | def _show(self, field_names: Sequence[str]) -> None:
method accept (line 118) | def accept(self) -> None:
method show_help (line 178) | def show_help(self) -> None:
FILE: qt/aqt/browser/find_duplicates.py
class FindDuplicatesDialog (line 33) | class FindDuplicatesDialog(QDialog):
method __init__ (line 34) | def __init__(self, browser: Browser, mw: aqt.AnkiQt):
method show_duplicates_report (line 83) | def show_duplicates_report(self, dupes: list[tuple[str, list[NoteId]]]...
method _tag_duplicates (line 118) | def _tag_duplicates(self) -> None:
method _on_duplicate_clicked (line 132) | def _on_duplicate_clicked(self, link: str) -> None:
FILE: qt/aqt/browser/layout.py
class BrowserLayout (line 11) | class BrowserLayout(Enum):
class QSplitterHandleEventFilter (line 17) | class QSplitterHandleEventFilter(QObject):
method __init__ (line 20) | def __init__(self, splitter: QSplitter):
method eventFilter (line 24) | def eventFilter(self, object: QObject | None, event: QEvent | None) ->...
FILE: qt/aqt/browser/previewer.py
class Previewer (line 37) | class Previewer(QDialog):
method __init__ (line 44) | def __init__(
method card (line 60) | def card(self) -> Card | None:
method card_changed (line 63) | def card_changed(self) -> bool:
method open (line 66) | def open(self) -> None:
method _create_gui (line 75) | def _create_gui(self) -> None:
method _on_finished (line 119) | def _on_finished(self, ok: int) -> None:
method _on_replay_audio (line 123) | def _on_replay_audio(self) -> None:
method _on_close (line 135) | def _on_close(self) -> None:
method _setup_web_view (line 144) | def _setup_web_view(self) -> None:
method _on_bridge_cmd (line 161) | def _on_bridge_cmd(self, cmd: str) -> Any:
method _update_flag_and_mark_icons (line 168) | def _update_flag_and_mark_icons(self, card: Card | None) -> None:
method render_card (line 180) | def render_card(self) -> None:
method cancel_timer (line 195) | def cancel_timer(self) -> None:
method _render_scheduled (line 200) | def _render_scheduled(self) -> None:
method _on_show_both_sides (line 272) | def _on_show_both_sides(self, toggle: bool) -> None:
method _state_and_mod (line 289) | def _state_and_mod(self) -> tuple[str, int, int]:
method state (line 298) | def state(self) -> str:
class MultiCardPreviewer (line 302) | class MultiCardPreviewer(Previewer):
method card (line 303) | def card(self) -> Card | None:
method card_changed (line 307) | def card_changed(self) -> bool:
method _create_gui (line 311) | def _create_gui(self) -> None:
method _on_prev (line 338) | def _on_prev(self) -> None:
method _on_prev_card (line 345) | def _on_prev_card(self) -> None:
method _on_next (line 348) | def _on_next(self) -> None:
method _on_next_card (line 355) | def _on_next_card(self) -> None:
method _updateButtons (line 358) | def _updateButtons(self) -> None:
method _should_enable_prev (line 368) | def _should_enable_prev(self) -> bool:
method _should_enable_next (line 371) | def _should_enable_next(self) -> bool:
method _on_close (line 374) | def _on_close(self) -> None:
class BrowserPreviewer (line 380) | class BrowserPreviewer(MultiCardPreviewer):
method __init__ (line 384) | def __init__(
method card (line 389) | def card(self) -> Card | None:
method card_changed (line 397) | def card_changed(self) -> bool:
method _on_prev_card (line 406) | def _on_prev_card(self) -> None:
method _on_next_card (line 411) | def _on_next_card(self) -> None:
method _should_enable_prev (line 416) | def _should_enable_prev(self) -> bool:
method _should_enable_next (line 421) | def _should_enable_next(self) -> bool:
method _render_scheduled (line 426) | def _render_scheduled(self) -> None:
FILE: qt/aqt/browser/sidebar/item.py
class SidebarItemType (line 12) | class SidebarItemType(Enum):
method section_roots (line 37) | def section_roots() -> Iterable[SidebarItemType]:
method is_section_root (line 40) | def is_section_root(self) -> bool:
method is_editable (line 43) | def is_editable(self) -> bool:
method can_be_added_to (line 51) | def can_be_added_to(self) -> bool:
method is_deletable (line 54) | def is_deletable(self) -> bool:
class SidebarItem (line 62) | class SidebarItem:
method __init__ (line 63) | def __init__(
method add_child (line 90) | def add_child(self, cb: SidebarItem) -> None:
method add_simple (line 94) | def add_simple(
method expanded (line 112) | def expanded(self) -> bool:
method expanded (line 116) | def expanded(self, expanded: bool) -> None:
method show_expanded (line 122) | def show_expanded(self, searching: bool) -> bool:
method is_highlighted (line 130) | def is_highlighted(self) -> bool:
method search (line 133) | def search(self, lowered_text: str) -> bool:
method has_same_id (line 141) | def has_same_id(self, other: SidebarItem) -> bool:
FILE: qt/aqt/browser/sidebar/model.py
class SidebarModel (line 12) | class SidebarModel(QAbstractItemModel):
method __init__ (line 13) | def __init__(
method _cache_rows (line 21) | def _cache_rows(self, node: SidebarItem) -> None:
method item_for_index (line 27) | def item_for_index(self, idx: QModelIndex) -> SidebarItem:
method index_for_item (line 30) | def index_for_item(self, item: SidebarItem) -> QModelIndex:
method search (line 34) | def search(self, text: str) -> bool:
method rowCount (line 40) | def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
method columnCount (line 47) | def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
method index (line 50) | def index(
method parent (line 65) | def parent(self, child: QModelIndex) -> QModelIndex: # type: ignore
method data (line 80) | def data(
method setData (line 102) | def setData(
method supportedDropActions (line 107) | def supportedDropActions(self) -> Qt.DropAction:
method flags (line 110) | def flags(self, index: QModelIndex) -> Qt.ItemFlag:
FILE: qt/aqt/browser/sidebar/searchbar.py
class SidebarSearchBar (line 12) | class SidebarSearchBar(QLineEdit):
method __init__ (line 13) | def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None:
method onTextChanged (line 25) | def onTextChanged(self, text: str) -> None:
method onSearch (line 29) | def onSearch(self) -> None:
method keyPressEvent (line 32) | def keyPressEvent(self, evt: QKeyEvent | None) -> None:
FILE: qt/aqt/browser/sidebar/toolbar.py
class SidebarTool (line 17) | class SidebarTool(Enum):
class SidebarToolbar (line 22) | class SidebarToolbar(QToolBar):
method __init__ (line 36) | def __init__(self, sidebar: aqt.browser.sidebar.SidebarTreeView) -> None:
method _setup_tools (line 47) | def _setup_tools(self) -> None:
method _on_action_group_triggered (line 61) | def _on_action_group_triggered(self, action: QAction) -> None:
method cleanup (line 65) | def cleanup(self) -> None:
method _update_icons (line 68) | def _update_icons(self) -> None:
FILE: qt/aqt/browser/sidebar/tree.py
class SidebarStage (line 60) | class SidebarStage(Enum):
class SidebarTreeView (line 75) | class SidebarTreeView(QTreeView):
method __init__ (line 76) | def __init__(self, browser: aqt.browser.Browser) -> None:
method _setup_style (line 106) | def _setup_style(self) -> None:
method cleanup (line 119) | def cleanup(self) -> None:
method tool (line 125) | def tool(self) -> SidebarTool:
method tool (line 129) | def tool(self, tool: SidebarTool) -> None:
method model (line 143) | def model(self) -> SidebarModel:
method op_executed (line 149) | def op_executed(
method refresh_if_needed (line 157) | def refresh_if_needed(self) -> None:
method refresh (line 162) | def refresh(self, new_current: SidebarItem | None = None) -> None:
method restore_current (line 201) | def restore_current(self, current: SidebarItem) -> None:
method find_item (line 210) | def find_item(
method search_for (line 225) | def search_for(self, text: str) -> None:
method _expand_where_necessary (line 238) | def _expand_where_necessary(
method update_search (line 272) | def update_search(
method drawRow (line 306) | def drawRow(
method dropEvent (line 319) | def dropEvent(self, event: QDropEvent | None) -> None:
method mouseReleaseEvent (line 331) | def mouseReleaseEvent(self, event: QMouseEvent | None) -> None:
method keyPressEvent (line 346) | def keyPressEvent(self, event: QKeyEvent | None) -> None:
method _on_selection_changed (line 361) | def _on_selection_changed(self, _new: QItemSelection, _old: QItemSelec...
method handle_drag_drop (line 385) | def handle_drag_drop(self, sources: list[SidebarItem], target: Sidebar...
method _handle_drag_drop_decks (line 397) | def _handle_drag_drop_decks(
method _handle_drag_drop_tags (line 416) | def _handle_drag_drop_tags(
method _handle_drag_drop_saved_search (line 438) | def _handle_drag_drop_saved_search(
method _on_search (line 448) | def _on_search(self, index: QModelIndex) -> None:
method _on_rename (line 454) | def _on_rename(self, item: SidebarItem, text: str) -> bool:
method _on_delete_key (line 470) | def _on_delete_key(self, index: QModelIndex) -> None:
method _enable_delete (line 475) | def _enable_delete(self, item: SidebarItem) -> bool:
method _on_add (line 480) | def _on_add(self, item: SidebarItem):
method _on_delete (line 483) | def _on_delete(self, item: SidebarItem) -> None:
method _on_expansion (line 491) | def _on_expansion(self, idx: QModelIndex) -> None:
method _on_collapse (line 497) | def _on_collapse(self, idx: QModelIndex) -> None:
method _root_tree (line 506) | def _root_tree(self) -> SidebarItem:
method _build_stage (line 518) | def _build_stage(self, root: SidebarItem, stage: SidebarStage) -> None:
method _section_root (line 538) | def _section_root(
method _saved_searches_tree (line 569) | def _saved_searches_tree(self, root: SidebarItem) -> None:
method _today_tree (line 593) | def _today_tree(self, root: SidebarItem) -> None:
method _card_state_tree (line 663) | def _card_state_tree(self, root: SidebarItem) -> None:
method _flags_tree (line 712) | def _flags_tree(self, root: SidebarItem) -> None:
method _tag_tree (line 746) | def _tag_tree(self, root: SidebarItem) -> None:
method _deck_tree (line 794) | def _deck_tree(self, root: SidebarItem) -> None:
method _notetype_tree (line 849) | def _notetype_tree(self, root: SidebarItem) -> None:
method onContextMenu (line 901) | def onContextMenu(self, point: QPoint) -> None:
method show_context_menu (line 907) | def show_context_menu(self, item: SidebarItem, index: QModelIndex) -> ...
method _maybe_add_type_specific_actions (line 923) | def _maybe_add_type_specific_actions(self, menu: QMenu, item: SidebarI...
method _maybe_add_add_action (line 951) | def _maybe_add_add_action(self, menu: QMenu, item: SidebarItem) -> None:
method _maybe_add_delete_action (line 955) | def _maybe_add_delete_action(
method _maybe_add_rename_actions (line 961) | def _maybe_add_rename_actions(
method _maybe_add_find_and_replace_action (line 975) | def _maybe_add_find_and_replace_action(
method _maybe_add_search_actions (line 986) | def _maybe_add_search_actions(self, menu: QMenu) -> None:
method _maybe_add_tree_actions (line 1006) | def _maybe_add_tree_actions(self, menu: QMenu) -> None:
method _on_rename_with_parents (line 1041) | def _on_rename_with_parents(self, item: SidebarItem) -> None:
method _on_find_and_replace (line 1075) | def _on_find_and_replace(self, item: SidebarItem) -> None:
method rename_flag (line 1089) | def rename_flag(self, item: SidebarItem, new_name: str) -> None:
method restore_default_flag_name (line 1093) | def restore_default_flag_name(self, item: SidebarItem) -> None:
method rename_deck (line 1100) | def rename_deck(self, item: SidebarItem, new_name: str) -> None:
method delete_decks (line 1113) | def delete_decks(self, _item: SidebarItem) -> None:
method remove_tags (line 1121) | def remove_tags(self, item: SidebarItem) -> None:
method rename_tag (line 1129) | def rename_tag(self, item: SidebarItem, new_name: str) -> None:
method add_tags_to_selected_notes (line 1155) | def add_tags_to_selected_notes(self) -> None:
method remove_tags_from_selected_notes (line 1159) | def remove_tags_from_selected_notes(self) -> None:
method _get_saved_searches (line 1168) | def _get_saved_searches(self) -> dict[str, str]:
method _set_saved_searches (line 1171) | def _set_saved_searches(self, searches: dict[str, str]) -> None:
method _get_current_search (line 1174) | def _get_current_search(self) -> str | None:
method _save_search (line 1181) | def _save_search(self, name: str, search: str, update: bool = False) -...
method remove_saved_searches (line 1194) | def remove_saved_searches(self, _item: SidebarItem) -> None:
method rename_saved_search (line 1202) | def rename_saved_search(self, item: SidebarItem, new_name: str) -> None:
method save_current_search (line 1219) | def save_current_search(self) -> None:
method update_saved_search (line 1227) | def update_saved_search(self, item: SidebarItem) -> None:
method manage_notetype (line 1235) | def manage_notetype(self, item: SidebarItem) -> None:
method manage_template (line 1243) | def manage_template(self, item: SidebarItem) -> None:
method manage_fields (line 1249) | def manage_fields(self, item: SidebarItem) -> None:
method _selected_items (line 1260) | def _selected_items(self) -> list[SidebarItem]:
method _selected_decks (line 1263) | def _selected_decks(self) -> list[DeckId]:
method _selected_saved_searches (line 1270) | def _selected_saved_searches(self) -> list[str]:
method _selected_tags (line 1277) | def _selected_tags(self) -> list[str]:
method _selection_model (line 1284) | def _selection_model(self) -> QItemSelectionModel:
FILE: qt/aqt/browser/table/__init__.py
class SearchContext (line 28) | class SearchContext:
class Cell (line 38) | class Cell:
method __init__ (line 39) | def __init__(
class CellRow (line 49) | class CellRow:
method __init__ (line 52) | def __init__(
method is_stale (line 65) | def is_stale(self, threshold: float) -> bool:
method generic (line 69) | def generic(length: int, cell_text: str) -> CellRow:
method placeholder (line 78) | def placeholder(length: int) -> CellRow:
method disabled (line 82) | def disabled(length: int, cell_text: str) -> CellRow:
function backend_elide_mode_to_aqt_elide_mode (line 88) | def backend_elide_mode_to_aqt_elide_mode(
function backend_color_to_aqt_color (line 100) | def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> dict[str, s...
function adjusted_bg_color (line 127) | def adjusted_bg_color(color: dict[str, str] | None) -> dict[str, str] | ...
FILE: qt/aqt/browser/table/model.py
class DataModel (line 24) | class DataModel(QAbstractTableModel):
method __init__ (line 36) | def __init__(
method get_cell (line 65) | def get_cell(self, index: QModelIndex) -> Cell:
method get_row (line 68) | def get_row(self, index: QModelIndex) -> CellRow:
method _fetch_row_and_update_cache (line 82) | def _fetch_row_and_update_cache(
method _fetch_row_from_backend (line 103) | def _fetch_row_from_backend(self, item: ItemId) -> CellRow:
method get_cached_row (line 127) | def get_cached_row(self, index: QModelIndex) -> CellRow | None:
method mark_cache_stale (line 133) | def mark_cache_stale(self) -> None:
method reset (line 136) | def reset(self) -> None:
method begin_reset (line 140) | def begin_reset(self) -> None:
method end_reset (line 144) | def end_reset(self) -> None:
method begin_blocking (line 149) | def begin_blocking(self) -> None:
method end_blocking (line 152) | def end_blocking(self) -> None:
method redraw_cells (line 156) | def redraw_cells(self) -> None:
method is_empty (line 169) | def is_empty(self) -> bool:
method len_rows (line 172) | def len_rows(self) -> int:
method len_columns (line 175) | def len_columns(self) -> int:
method get_item (line 180) | def get_item(self, index: QModelIndex) -> ItemId:
method get_items (line 183) | def get_items(self, indices: list[QModelIndex]) -> Sequence[ItemId]:
method get_card_ids (line 186) | def get_card_ids(self, indices: list[QModelIndex]) -> Sequence[CardId]:
method get_note_ids (line 189) | def get_note_ids(self, indices: list[QModelIndex]) -> Sequence[NoteId]:
method get_note_id (line 192) | def get_note_id(self, index: QModelIndex) -> NoteId | None:
method get_item_row (line 199) | def get_item_row(self, item: ItemId) -> int | None:
method get_item_rows (line 205) | def get_item_rows(self, items: Sequence[ItemId]) -> list[int]:
method get_card_row (line 212) | def get_card_row(self, card_id: CardId) -> int | None:
method get_card (line 217) | def get_card(self, index: QModelIndex) -> Card | None:
method get_note (line 230) | def get_note(self, index: QModelIndex) -> Note | None:
method toggle_state (line 242) | def toggle_state(self, context: SearchContext) -> ItemState:
method search (line 257) | def search(self, context: SearchContext) -> None:
method _search_inner (line 264) | def _search_inner(self, context: SearchContext) -> None:
method reverse (line 282) | def reverse(self) -> None:
method column_at (line 289) | def column_at(self, index: QModelIndex) -> Column:
method column_at_section (line 292) | def column_at_section(self, section: int) -> Column:
method active_column_index (line 303) | def active_column_index(self, column: str) -> int | None:
method toggle_column (line 310) | def toggle_column(self, column: str) -> None:
method rowCount (line 318) | def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
method columnCount (line 323) | def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
method data (line 328) | def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any:
method headerData (line 350) | def headerData(
method flags (line 360) | def flags(self, index: QModelIndex) -> Qt.ItemFlag:
function addon_column_fillin (line 368) | def addon_column_fillin(key: str) -> Column:
FILE: qt/aqt/browser/table/state.py
class ItemState (line 18) | class ItemState(ABC):
method __init__ (line 24) | def __init__(self, col: Collection) -> None:
method is_notes_mode (line 29) | def is_notes_mode(self) -> bool:
method note_ids_from_card_ids (line 35) | def note_ids_from_card_ids(self, items: Sequence[ItemId]) -> Sequence[...
method card_ids_from_note_ids (line 41) | def card_ids_from_note_ids(self, items: Sequence[ItemId]) -> Sequence[...
method column_key_at (line 45) | def column_key_at(self, index: int) -> str:
method column_label (line 48) | def column_label(self, column: Column) -> str:
method column_tooltip (line 53) | def column_tooltip(self, column: Column) -> str:
method active_columns (line 63) | def active_columns(self) -> list[str]:
method toggle_active_column (line 67) | def toggle_active_column(self, column: str) -> None:
method sort_column (line 71) | def sort_column(self) -> str:
method sort_column (line 75) | def sort_column(self, column: str) -> None:
method sort_backwards (line 80) | def sort_backwards(self) -> bool:
method sort_backwards (line 85) | def sort_backwards(self, order: bool) -> None:
method get_card (line 92) | def get_card(self, item: ItemId) -> Card:
method get_note (line 96) | def get_note(self, item: ItemId) -> Note:
method find_items (line 102) | def find_items(
method get_item_from_card_id (line 108) | def get_item_from_card_id(self, card: CardId) -> ItemId:
method get_card_ids (line 112) | def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
method get_note_ids (line 116) | def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
method toggle_state (line 122) | def toggle_state(self) -> ItemState:
method get_new_items (line 126) | def get_new_items(self, old_items: Sequence[ItemId]) -> ItemList:
class CardState (line 130) | class CardState(ItemState):
method __init__ (line 135) | def __init__(self, col: Collection) -> None:
method active_columns (line 140) | def active_columns(self) -> list[str]:
method toggle_active_column (line 143) | def toggle_active_column(self, column: str) -> None:
method get_card (line 150) | def get_card(self, item: ItemId) -> Card:
method get_note (line 153) | def get_note(self, item: ItemId) -> Note:
method find_items (line 156) | def find_items(
method get_item_from_card_id (line 161) | def get_item_from_card_id(self, card: CardId) -> ItemId:
method get_card_ids (line 164) | def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
method get_note_ids (line 167) | def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
method toggle_state (line 170) | def toggle_state(self) -> NoteState:
method get_new_items (line 173) | def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[CardId]:
class NoteState (line 177) | class NoteState(ItemState):
method __init__ (line 182) | def __init__(self, col: Collection) -> None:
method active_columns (line 187) | def active_columns(self) -> list[str]:
method toggle_active_column (line 190) | def toggle_active_column(self, column: str) -> None:
method get_card (line 197) | def get_card(self, item: ItemId) -> Card:
method get_note (line 202) | def get_note(self, item: ItemId) -> Note:
method find_items (line 205) | def find_items(
method get_item_from_card_id (line 210) | def get_item_from_card_id(self, card: CardId) -> ItemId:
method get_card_ids (line 213) | def get_card_ids(self, items: Sequence[ItemId]) -> Sequence[CardId]:
method get_note_ids (line 216) | def get_note_ids(self, items: Sequence[ItemId]) -> Sequence[NoteId]:
method toggle_state (line 219) | def toggle_state(self) -> CardState:
method get_new_items (line 222) | def get_new_items(self, old_items: Sequence[ItemId]) -> Sequence[NoteId]:
FILE: qt/aqt/browser/table/table.py
class Table (line 32) | class Table:
method __init__ (line 35) | def __init__(self, browser: aqt.browser.Browser) -> None:
method set_view (line 58) | def set_view(self, view: QTableView) -> None:
method cleanup (line 63) | def cleanup(self) -> None:
method len (line 71) | def len(self) -> int:
method len_selection (line 74) | def len_selection(self, refresh: bool = False) -> int:
method has_current (line 80) | def has_current(self) -> bool:
method has_previous (line 83) | def has_previous(self) -> bool:
method has_next (line 86) | def has_next(self) -> bool:
method is_notes_mode (line 89) | def is_notes_mode(self) -> bool:
method get_current_card (line 94) | def get_current_card(self) -> Card | None:
method get_current_note (line 97) | def get_current_note(self) -> Note | None:
method get_single_selected_card (line 100) | def get_single_selected_card(self) -> Card | None:
method get_selected_card_ids (line 109) | def get_selected_card_ids(self) -> Sequence[CardId]:
method get_selected_note_ids (line 112) | def get_selected_note_ids(self) -> Sequence[NoteId]:
method get_card_ids_from_selected_note_ids (line 115) | def get_card_ids_from_selected_note_ids(self) -> Sequence[CardId]:
method select_all (line 120) | def select_all(self) -> None:
method clear_selection (line 124) | def clear_selection(self) -> None:
method invert_selection (line 129) | def invert_selection(self) -> None:
method select_single_card (line 139) | def select_single_card(
method reset (line 154) | def reset(self) -> None:
method begin_reset (line 159) | def begin_reset(self) -> None:
method end_reset (line 163) | def end_reset(self) -> None:
method on_backend_will_block (line 167) | def on_backend_will_block(self) -> None:
method on_backend_did_block (line 172) | def on_backend_did_block(self) -> None:
method redraw_cells (line 175) | def redraw_cells(self) -> None:
method op_executed (line 178) | def op_executed(
method search (line 188) | def search(self, txt: str) -> None:
method toggle_state (line 193) | def toggle_state(self, is_notes_mode: bool, last_search: str) -> None:
method to_previous_row (line 210) | def to_previous_row(self) -> None:
method to_next_row (line 213) | def to_next_row(self) -> None:
method to_first_row (line 216) | def to_first_row(self) -> None:
method to_last_row (line 219) | def to_last_row(self) -> None:
method to_row_of_unselected_note (line 222) | def to_row_of_unselected_note(self) -> Sequence[NoteId]:
method clear_current (line 255) | def clear_current(self) -> None:
method _current (line 266) | def _current(self) -> QModelIndex:
method _selected (line 269) | def _selected(self) -> list[QModelIndex]:
method _set_current (line 274) | def _set_current(self, row: int, column: int = 0) -> None:
method _reset_selection (line 281) | def _reset_selection(self) -> None:
method _select_rows (line 290) | def _select_rows(self, rows: list[int]) -> None:
method _set_sort_indicator (line 301) | def _set_sort_indicator(self) -> None:
method _set_column_sizes (line 316) | def _set_column_sizes(self) -> None:
method _save_header (line 326) | def _save_header(self) -> None:
method _restore_header (line 329) | def _restore_header(self) -> None:
method _setup_view (line 339) | def _setup_view(self) -> None:
method _update_font (line 357) | def _update_font(self) -> None:
method _setup_headers (line 371) | def _setup_headers(self) -> None:
method _on_current_changed (line 389) | def _on_current_changed(self, current: QModelIndex, previous: QModelIn...
method _on_selection_changed (line 393) | def _on_selection_changed(
method _on_row_state_will_change (line 411) | def _on_row_state_will_change(self, index: QModelIndex, was_restored: ...
method _on_row_state_changed (line 422) | def _on_row_state_changed(self, index: QModelIndex, was_restored: bool...
method _on_context_menu (line 441) | def _on_context_menu(self, _point: QPoint) -> None:
method _on_header_context (line 462) | def _on_header_context(self, pos: QPoint) -> None:
method _on_column_moved (line 480) | def _on_column_moved(self, *_args: Any) -> None:
method _on_column_toggled (line 483) | def _on_column_toggled(self, checked: bool, column: str) -> None:
method _on_sort_column_changed (line 494) | def _on_sort_column_changed(self, section: int, order: Qt.SortOrder) -...
method _reverse (line 515) | def _reverse(self) -> None:
method _save_selection (line 522) | def _save_selection(self) -> None:
method _restore_selection (line 528) | def _restore_selection(self, new_selected_and_current: Callable) -> None:
method _qualify_selected_rows (line 547) | def _qualify_selected_rows(self, rows: list[int], current: int | None)...
method _intersected_selection (line 557) | def _intersected_selection(self) -> tuple[list[int], int | None]:
method _toggled_selection (line 567) | def _toggled_selection(self) -> tuple[list[int], int | None]:
method _scroll_to_row (line 582) | def _scroll_to_row(self, row: int, scroll_even_if_visible: bool = Fals...
method _scroll_to_column (line 599) | def _scroll_to_column(self, column: int) -> None:
method _move_current_to_index (line 616) | def _move_current_to_index(self, index: QModelIndex) -> None:
method _move_current (line 632) | def _move_current(
method _move_current_to_row (line 643) | def _move_current_to_row(self, row: int) -> None:
method _selection_model (line 657) | def _selection_model(self) -> QItemSelectionModel:
method _horizontal_header (line 663) | def _horizontal_header(self) -> QHeaderView:
class StatusDelegate (line 670) | class StatusDelegate(QItemDelegate):
method __init__ (line 671) | def __init__(self, browser: aqt.browser.Browser, model: DataModel) -> ...
method paint (line 675) | def paint(
FILE: qt/aqt/changenotetype.py
class ChangeNotetypeDialog (line 28) | class ChangeNotetypeDialog(QDialog):
method __init__ (line 32) | def __init__(
method _setup_ui (line 45) | def _setup_ui(self, notetype_id: NotetypeId) -> None:
method reject (line 62) | def reject(self) -> None:
method save (line 68) | def save(self, data: bytes) -> None:
function change_notetype_dialog (line 88) | def change_notetype_dialog(parent: QWidget, note_ids: Sequence[NoteId]) ...
FILE: qt/aqt/clayout.py
class CardLayout (line 48) | class CardLayout(QDialog):
method __init__ (line 49) | def __init__(
method redraw_everything (line 100) | def redraw_everything(self) -> None:
method update_current_ordinal_and_redraw (line 106) | def update_current_ordinal_and_redraw(self, idx: int) -> None:
method _isCloze (line 114) | def _isCloze(self) -> bool:
method setupTopArea (line 120) | def setupTopArea(self) -> None:
method updateTopArea (line 137) | def updateTopArea(self) -> None:
method updateCardNames (line 140) | def updateCardNames(self) -> None:
method _summarizedName (line 151) | def _summarizedName(self, idx: int, tmpl: dict) -> str:
method _fieldsOnTemplate (line 159) | def _fieldsOnTemplate(self, fmt: str) -> str:
method setupShortcuts (line 183) | def setupShortcuts(self) -> None:
method setupMainArea (line 230) | def setupMainArea(self) -> None:
method setup_edit_area (line 253) | def setup_edit_area(self) -> None:
method setup_cloze_number_box (line 299) | def setup_cloze_number_box(self) -> None:
method on_change_cloze (line 312) | def on_change_cloze(self, idx: int) -> None:
method on_editor_toggled (line 317) | def on_editor_toggled(self) -> None:
method on_search_changed (line 334) | def on_search_changed(self, text: str) -> None:
method on_search_next (line 344) | def on_search_next(self) -> None:
method setup_preview (line 348) | def setup_preview(self) -> None:
method on_fill_empty_action_toggled (line 390) | def on_fill_empty_action_toggled(self) -> None:
method on_night_mode_action_toggled (line 394) | def on_night_mode_action_toggled(self) -> None:
method on_mobile_class_action_toggled (line 402) | def on_mobile_class_action_toggled(self) -> None:
method on_preview_settings (line 406) | def on_preview_settings(self) -> None:
method on_preview_toggled (line 431) | def on_preview_toggled(self) -> None:
method _on_bridge_cmd (line 435) | def _on_bridge_cmd(self, cmd: str) -> Any:
method note_has_empty_field (line 439) | def note_has_empty_field(self) -> bool:
method setupButtons (line 449) | def setupButtons(self) -> None:
method current_template (line 480) | def current_template(self) -> dict:
method fill_fields_from_template (line 485) | def fill_fields_from_template(self) -> None:
method write_edits_to_template_and_redraw (line 499) | def write_edits_to_template_and_redraw(self) -> None:
method renderPreview (line 521) | def renderPreview(self) -> None:
method cancelPreviewTimer (line 528) | def cancelPreviewTimer(self) -> None:
method _renderPreview (line 533) | def _renderPreview(self) -> None:
method maybeTextInput (line 586) | def maybeTextInput(self, txt: str, type: str = "q") -> str:
method onRemove (line 615) | def onRemove(self) -> None:
method onRemoveInner (line 644) | def onRemoveInner(self, template: dict) -> None:
method onRename (line 654) | def onRename(self) -> None:
method onReorder (line 665) | def onReorder(self) -> None:
method _newCardName (line 690) | def _newCardName(self) -> str:
method onAddCard (line 699) | def onAddCard(self) -> None:
method on_restore_to_default (line 715) | def on_restore_to_default(
method onFlip (line 743) | def onFlip(self) -> None:
method _flipQA (line 748) | def _flipQA(self, src: dict, dst: dict) -> None:
method onCopyMarkdown (line 757) | def onCopyMarkdown(self) -> None:
method onMore (line 782) | def onMore(self) -> None:
method onBrowserDisplay (line 832) | def onBrowserDisplay(self) -> None:
method onBrowserDisplayOk (line 847) | def onBrowserDisplayOk(self, f: browserdisp.Ui_Dialog) -> None:
method onTargetDeck (line 860) | def onTargetDeck(self) -> None:
method onAddField (line 894) | def onAddField(self) -> None:
method _addField (line 914) | def _addField(self, field: str, font: str, size: int) -> None:
method accept (line 931) | def accept(self) -> None:
method reject (line 942) | def reject(self) -> None:
method cleanup (line 968) | def cleanup(self) -> None:
method onHelp (line 979) | def onHelp(self) -> None:
class SelectStockNotetype (line 983) | class SelectStockNotetype(QDialog):
method __init__ (line 984) | def __init__(
method reject (line 1011) | def reject(self) -> None:
method accept (line 1014) | def accept(self) -> None:
FILE: qt/aqt/customstudy.py
class CustomStudy (line 33) | class CustomStudy(QDialog):
method fetch_data_and_show (line 35) | def fetch_data_and_show(mw: aqt.AnkiQt) -> None:
method __init__ (line 54) | def __init__(
method setupSignals (line 74) | def setupSignals(self) -> None:
method count_with_children (line 84) | def count_with_children(self, parent: int, children: int) -> str:
method onRadioChange (line 90) | def onRadioChange(self, idx: int) -> None:
method setTextAfterSpinner (line 158) | def setTextAfterSpinner(self, newSpinValue) -> None:
method accept (line 177) | def accept(self) -> None:
method _create_and_close (line 214) | def _create_and_close(self, request: CustomStudyRequest) -> None:
FILE: qt/aqt/data/web/js/deckbrowser.ts
function init (line 6) | function init() {
function handleDropEvent (line 27) | function handleDropEvent(event, ui) {
FILE: qt/aqt/data/web/js/reviewer-bottom.ts
function updateTime (line 13) | function updateTime(): void {
function showQuestion (line 34) | function showQuestion(txt: string, maxTime_: number): void {
function showAnswer (line 52) | function showAnswer(txt: string, stopTimer = false): void {
function selectedAnswerButton (line 57) | function selectedAnswerButton(): string {
FILE: qt/aqt/data/web/js/toolbar.ts
type SyncState (line 8) | enum SyncState {
function updateSyncColor (line 14) | function updateSyncColor(state: SyncState) {
function isAbsolutelyPositioned (line 34) | function isAbsolutelyPositioned(node: Node): boolean {
function isLegacyAddonElement (line 41) | function isLegacyAddonElement(node: Node): boolean {
function getElementDimensions (line 53) | function getElementDimensions(element: HTMLElement): [number, number] {
function moveLegacyAddonsToTray (line 69) | function moveLegacyAddonsToTray() {
FILE: qt/aqt/data/web/js/vendor/plot.js
function clamp (line 7) | function clamp(min,value,max){return value<min?min:value>max?max:value}
function Canvas (line 7) | function Canvas(cls,container){var element=container.children("."+cls)[0...
function Plot (line 7) | function Plot(placeholder,data_,options_,plugins){var series=[],options=...
function floorInBase (line 8) | function floorInBase(n,base){return base*Math.floor(n/base)}
function init (line 16) | function init(plot){function findMatchingSeries(s,allseries){var res=nul...
function init (line 24) | function init(plot){var canvas=null,target=null,options=null,maxRadius=n...
FILE: qt/aqt/dbcheck.py
function on_progress (line 14) | def on_progress(mw: aqt.main.AnkiQt) -> None:
function check_db (line 27) | def check_db(mw: aqt.AnkiQt) -> None:
FILE: qt/aqt/debug_console.py
function show_debug_console (line 30) | def show_debug_console() -> None:
class Action (line 42) | class Action:
class DebugConsole (line 48) | class DebugConsole(QDialog):
method __init__ (line 52) | def __init__(self, parent: QWidget) -> None:
method _setup_ui (line 60) | def _setup_ui(self):
method _setup_text_edits (line 73) | def _setup_text_edits(self):
method _setup_scripts (line 81) | def _setup_scripts(self) -> None:
method _setup_actions (line 87) | def _setup_actions(self) -> None:
method _actions (line 93) | def _actions(self):
method reject (line 104) | def reject(self) -> None:
method _on_script_change (line 109) | def _on_script_change(self, new_index: int) -> None:
method _get_script (line 114) | def _get_script(self, idx: int) -> str | None:
method _get_item (line 121) | def _get_item(self, idx: int) -> Path | None:
method _get_index (line 127) | def _get_index(self, path: Path) -> int:
method _path_to_item (line 130) | def _path_to_item(self, path: Path) -> str:
method _current_script_path (line 133) | def _current_script_path(self) -> Path | None:
method _save_script (line 136) | def _save_script(self) -> None:
method _open_script (line 155) | def _open_script(self) -> None:
method _delete_script (line 175) | def _delete_script(self) -> None:
method _drop_buffer_and_shift_keys (line 184) | def _drop_buffer_and_shift_keys(self, idx: int) -> None:
method _setup_context_menu (line 190) | def _setup_context_menu(self) -> None:
method _on_context_menu (line 198) | def _on_context_menu(self, text_edit: QPlainTextEdit) -> None:
method _on_widgetGallery (line 208) | def _on_widgetGallery(self) -> None:
method _captureOutput (line 214) | def _captureOutput(self, on: bool) -> None:
method _card_repr (line 232) | def _card_repr(self, card: anki.cards.Card | None) -> None:
method _debugCard (line 259) | def _debugCard(self) -> anki.cards.Card | None:
method _debugBrowserCard (line 265) | def _debugBrowserCard(self) -> anki.cards.Card | None:
method onDebugPrint (line 270) | def onDebugPrint(self) -> None:
method onDebugRet (line 284) | def onDebugRet(self) -> None:
function _split_off_leading_whitespace (line 325) | def _split_off_leading_whitespace(text: str) -> tuple[str, str]:
FILE: qt/aqt/deckbrowser.py
class DeckBrowserBottomBar (line 32) | class DeckBrowserBottomBar:
method __init__ (line 33) | def __init__(self, deck_browser: DeckBrowser) -> None:
class RenderData (line 38) | class RenderData:
class DeckBrowserContent (line 48) | class DeckBrowserContent:
class RenderDeckNodeContext (line 62) | class RenderDeckNodeContext:
class DeckBrowser (line 66) | class DeckBrowser:
method __init__ (line 69) | def __init__(self, mw: AnkiQt) -> None:
method show (line 76) | def show(self) -> None:
method refresh (line 83) | def refresh(self) -> None:
method refresh_if_needed (line 87) | def refresh_if_needed(self) -> None:
method op_executed (line 91) | def op_executed(
method _linkHandler (line 105) | def _linkHandler(self, url: str) -> Any:
method set_current_deck (line 139) | def set_current_deck(self, deck_id: DeckId) -> None:
method _renderPage (line 158) | def _renderPage(self, reuse: bool = False) -> None:
method __renderPage (line 181) | def __renderPage(self, offset: int | None) -> None:
method _scrollToOffset (line 204) | def _scrollToOffset(self, offset: int) -> None:
method _renderStats (line 207) | def _renderStats(self) -> str:
method _renderDeckTree (line 212) | def _renderDeckTree(self, top: DeckTreeNode) -> str:
method _render_deck_node (line 233) | def _render_deck_node(self, node: DeckTreeNode, ctx: RenderDeckNodeCon...
method _topLevelDragRow (line 303) | def _topLevelDragRow(self) -> str:
method _showOptions (line 309) | def _showOptions(self, did: str) -> None:
method _export (line 326) | def _export(self, did: DeckId) -> None:
method _rename (line 329) | def _rename(self, did: DeckId) -> None:
method _options (line 345) | def _options(self, did: DeckId) -> None:
method _collapse (line 348) | def _collapse(self, did: DeckId) -> None:
method _handle_drag_and_drop (line 360) | def _handle_drag_and_drop(self, source: DeckId, target: DeckId) -> None:
method _delete (line 365) | def _delete(self, did: DeckId) -> None:
method _drawButtons (line 382) | def _drawButtons(self) -> None:
method _onShared (line 396) | def _onShared(self) -> None:
method _on_create (line 399) | def _on_create(self) -> None:
method _v1_upgrade_message (line 407) | def _v1_upgrade_message(self, required: bool) -> str:
method _confirm_upgrade (line 431) | def _confirm_upgrade(self) -> None:
FILE: qt/aqt/deckchooser.py
class DeckChooser (line 16) | class DeckChooser(QHBoxLayout):
method __init__ (line 17) | def __init__(
method _setup_ui (line 40) | def _setup_ui(self, show_label: bool) -> None:
method selected_deck_name (line 63) | def selected_deck_name(self) -> str:
method selected_deck_id (line 69) | def selected_deck_id(self) -> DeckId:
method selected_deck_id (line 75) | def selected_deck_id(self, id: DeckId) -> None:
method _ensure_selected_deck_valid (line 81) | def _ensure_selected_deck_valid(self) -> None:
method _update_button_label (line 86) | def _update_button_label(self) -> None:
method show (line 90) | def show(self) -> None:
method hide (line 93) | def hide(self) -> None:
method choose_deck (line 96) | def choose_deck(self) -> None:
method on_operation_did_execute (line 125) | def on_operation_did_execute(
method cleanup (line 131) | def cleanup(self) -> None:
method selectedId (line 139) | def selectedId(self) -> DeckId:
FILE: qt/aqt/deckconf.py
class DeckConf (line 31) | class DeckConf(QDialog):
method __init__ (line 32) | def __init__(self, mw: aqt.AnkiQt, deck: dict) -> None:
method setupCombos (line 63) | def setupCombos(self) -> None:
method setupConfs (line 73) | def setupConfs(self) -> None:
method loadConfs (line 78) | def loadConfs(self) -> None:
method confOpts (line 95) | def confOpts(self) -> None:
method onConfChange (line 109) | def onConfChange(self, idx: int) -> None:
method addGroup (line 125) | def addGroup(self) -> None:
method remGroup (line 140) | def remGroup(self) -> None:
method renameGroup (line 151) | def renameGroup(self) -> None:
method setChildren (line 162) | def setChildren(self) -> None:
method listToUser (line 176) | def listToUser(self, l: list[int | float]) -> str:
method parentLimText (line 185) | def parentLimText(self, type: str = "new") -> str:
method loadConf (line 199) | def loadConf(self) -> None:
method onRestore (line 236) | def onRestore(self) -> None:
method onNewOrderChanged (line 245) | def onNewOrderChanged(self, new: bool) -> None:
method updateList (line 257) | def updateList(self, conf: Any, key: str, w: QLineEdit, minSize: int =...
method saveConf (line 279) | def saveConf(self) -> None:
method reject (line 321) | def reject(self) -> None:
method accept (line 324) | def accept(self) -> None:
FILE: qt/aqt/deckdescription.py
class DeckDescriptionDialog (line 16) | class DeckDescriptionDialog(QDialog):
method __init__ (line 20) | def __init__(self, mw: aqt.main.AnkiQt) -> None:
method _setup_and_show (line 33) | def _setup_and_show(self, deck: DeckDict) -> None:
method _setup_ui (line 41) | def _setup_ui(self) -> None:
method save_and_accept (line 69) | def save_and_accept(self) -> None:
method accept (line 77) | def accept(self) -> None:
FILE: qt/aqt/deckoptions.py
class DeckOptionsDialog (line 24) | class DeckOptionsDialog(QDialog):
method __init__ (line 30) | def __init__(self, mw: aqt.main.AnkiQt, deck: DeckDict) -> None:
method _setup_ui (line 38) | def _setup_ui(self) -> None:
method set_ready (line 57) | def set_ready(self):
method closeEvent (line 61) | def closeEvent(self, evt: QCloseEvent | None) -> None:
method require_close (line 68) | def require_close(self):
method reject (line 73) | def reject(self) -> None:
function confirm_deck_then_display_options (line 81) | def confirm_deck_then_display_options(active_card: Card | None = None) -...
function _deck_prompt_dialog (line 101) | def _deck_prompt_dialog(decks: list[DeckDict]) -> None:
function display_options_for_deck_id (line 118) | def display_options_for_deck_id(deck_id: DeckId) -> None:
function display_options_for_deck (line 124) | def display_options_for_deck(deck: DeckDict) -> None:
FILE: qt/aqt/editcurrent.py
class EditCurrent (line 15) | class EditCurrent(QMainWindow):
method __init__ (line 16) | def __init__(self, mw: aqt.AnkiQt) -> None:
method on_operation_did_execute (line 45) | def on_operation_did_execute(
method cleanup (line 62) | def cleanup(self) -> None:
method reopen (line 68) | def reopen(self, mw: aqt.AnkiQt) -> None:
method closeEvent (line 73) | def closeEvent(self, evt: QCloseEvent | None) -> None:
method _saveAndClose (line 76) | def _saveAndClose(self) -> None:
method closeWithCallback (line 81) | def closeWithCallback(self, onsuccess: Callable[[], None]) -> None:
FILE: qt/aqt/editor.py
class EditorMode (line 92) | class EditorMode(Enum):
class EditorState (line 98) | class EditorState(Enum):
class Editor (line 110) | class Editor:
method __init__ (line 120) | def __init__(
method setupOuter (line 160) | def setupOuter(self) -> None:
method add_webview (line 167) | def add_webview(self) -> None:
method setupWeb (line 172) | def setupWeb(self) -> None:
method resourceToData (line 226) | def resourceToData(self, path: str) -> str:
method addButton (line 236) | def addButton(
method _addButton (line 292) | def _addButton(
method setupShortcuts (line 341) | def setupShortcuts(self) -> None:
method setupColourPalette (line 353) | def setupColourPalette(self) -> None:
method _addFocusCheck (line 361) | def _addFocusCheck(self, fn: Callable) -> Callable:
method onFields (line 369) | def onFields(self) -> None:
method _onFields (line 372) | def _onFields(self) -> None:
method onCardLayout (line 377) | def onCardLayout(self) -> None:
method _onCardLayout (line 380) | def _onCardLayout(self) -> None:
method onBridgeCmd (line 402) | def onBridgeCmd(self, cmd: str) -> Any:
method mungeHTML (line 526) | def mungeHTML(self, txt: str) -> str:
method signal_state_change (line 529) | def signal_state_change(
method set_note (line 538) | def set_note(
method loadNoteKeepingFocus (line 552) | def loadNoteKeepingFocus(self) -> None:
method loadNote (line 555) | def loadNote(self, focusTo: int | None = None) -> None:
method _save_current_note (line 635) | def _save_current_note(self) -> None:
method fonts (line 644) | def fonts(self) -> list[tuple[str, int, bool]]:
method call_after_note_saved (line 650) | def call_after_note_saved(
method _check_and_update_duplicate_display_async (line 662) | def _check_and_update_duplicate_display_async(self) -> None:
method _update_duplicate_display (line 680) | def _update_duplicate_display(self, result: NoteFieldsCheckResult.V) -...
method showDupes (line 698) | def showDupes(self) -> None:
method fieldsAreBlank (line 713) | def fieldsAreBlank(self, previousNote: Note | None = None) -> bool:
method cleanup (line 726) | def cleanup(self) -> None:
method setupTags (line 741) | def setupTags(self) -> None:
method updateTags (line 761) | def updateTags(self) -> None:
method on_tag_focus_lost (line 768) | def on_tag_focus_lost(self) -> None:
method blur_tags_if_focused (line 775) | def blur_tags_if_focused(self) -> None:
method hideCompleters (line 781) | def hideCompleters(self) -> None:
method onFocusTags (line 784) | def onFocusTags(self) -> None:
method saveAddModeVars (line 789) | def saveAddModeVars(self) -> None:
method onAddMedia (line 797) | def onAddMedia(self) -> None:
method addMedia (line 818) | def addMedia(self, path: str, canDelete: bool = False) -> None:
method resolve_media (line 830) | def resolve_media(self, path: str) -> None:
method _addMedia (line 843) | def _addMedia(self, path: str, canDelete: bool = False) -> str:
method _addMediaFromData (line 850) | def _addMediaFromData(self, fname: str, data: bytes) -> str:
method onRecSound (line 853) | def onRecSound(self) -> None:
method urlToLink (line 864) | def urlToLink(self, url: str, allowed_suffixes: Iterable[str] = ()) ->...
method fnameToLink (line 876) | def fnameToLink(self, fname: str) -> str:
method urlToFile (line 885) | def urlToFile(
method isURL (line 895) | def isURL(self, s: str) -> bool:
method inlinedImageToFilename (line 904) | def inlinedImageToFilename(self, txt: str) -> str:
method inlinedImageToLink (line 918) | def inlinedImageToLink(self, src: str) -> str:
method _pasted_image_filename (line 925) | def _pasted_image_filename(self, data: bytes, ext: str) -> str:
method _read_pasted_image (line 929) | def _read_pasted_image(self, mime: QMimeData) -> str:
method _addPastedImage (line 949) | def _addPastedImage(self, data: bytes, ext: str) -> str:
method _retrieveURL (line 954) | def _retrieveURL(self, url: str) -> str | None:
method _pastePreFilter (line 1006) | def _pastePreFilter(self, html: str, internal: bool) -> str:
method doPaste (line 1053) | def doPaste(self, html: str, internal: bool, extended: bool = False) -...
method doDrop (line 1062) | def doDrop(
method onPaste (line 1074) | def onPaste(self) -> None:
method onCutOrCopy (line 1077) | def onCutOrCopy(self) -> None:
method current_notetype_is_image_occlusion (line 1083) | def current_notetype_is_image_occlusion(self) -> bool:
method setup_mask_editor (line 1092) | def setup_mask_editor(self, image_path: str) -> None:
method select_image_and_occlude (line 1106) | def select_image_and_occlude(self) -> None:
method extract_img_path_from_html (line 1123) | def extract_img_path_from_html(self, html: str) -> str | None:
method select_image_from_clipboard_and_occlude (line 1131) | def select_image_from_clipboard_and_occlude(self) -> None:
method setup_mask_editor_for_new_note (line 1149) | def setup_mask_editor_for_new_note(
method setup_mask_editor_for_existing_note (line 1171) | def setup_mask_editor_for_existing_note(
method reset_image_occlusion (line 1189) | def reset_image_occlusion(self) -> None:
method update_occlusions_field (line 1192) | def update_occlusions_field(self) -> None:
method _setup_mask_editor (line 1195) | def _setup_mask_editor(self, io_options: dict):
method _create_add_io_options (line 1203) | def _create_add_io_options(
method _create_clone_io_options (line 1212) | def _create_clone_io_options(orig_note_id: NoteId) -> dict:
method _create_edit_io_options (line 1218) | def _create_edit_io_options(note_id: NoteId) -> dict:
method onHtmlEdit (line 1227) | def onHtmlEdit(self) -> None:
method _onHtmlEdit (line 1232) | def _onHtmlEdit(self, field: int) -> None:
method t
Condensed preview — 1595 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (6,825K chars).
[
{
"path": ".buildkite/linux/docker/Dockerfile",
"chars": 1662,
"preview": "FROM ubuntu:22.04\n\nARG DEBIAN_FRONTEND=\"noninteractive\"\n\nRUN useradd -d /state -m -u 998 user\n\nRUN apt-get update && apt"
},
{
"path": ".buildkite/linux/docker/build.sh",
"chars": 356,
"preview": "#!/bin/bash\n# builds an 'anki-[amd|arm]' image for the current platform\n#\n# for a cross-compile on recent Docker:\n# do"
},
{
"path": ".buildkite/linux/docker/buildkite.cfg",
"chars": 159,
"preview": "name=\"lin-ci\"\ntags=\"queue=lin-ci\"\nbuild-path=\"/state/build\"\nhooks-path=\"/etc/buildkite-agent/hooks\"\nno-plugins=true\nno-l"
},
{
"path": ".buildkite/linux/docker/common.inc",
"chars": 108,
"preview": "#!/bin/bash\n\nset -e\n\nif [[ \"$(uname -m)\" == \"x86_64\" ]]; then\n platform=\"amd\"\nelse\n platform=\"arm\"\nfi\n"
},
{
"path": ".buildkite/linux/docker/environment",
"chars": 218,
"preview": "#!/bin/bash\n\nif [[ \"${BUILDKITE_COMMAND}\" != \".buildkite/linux/entrypoint\" &&\n \"${BUILDKITE_COMMAND}\" != \".buildkite/"
},
{
"path": ".buildkite/linux/docker/run.sh",
"chars": 602,
"preview": "#!/bin/bash\n# - use './run.sh' to run in the foreground\n# - use './run.sh serve' to daemonize.\n\nset -e\n\n. common.inc\n\nif"
},
{
"path": ".buildkite/linux/entrypoint",
"chars": 499,
"preview": "#!/bin/bash\n\nset -e\n\nexport PATH=\"$PATH:/state/rust/cargo/bin\"\nexport BUILD_ROOT=/state/build\nexport ONLINE_TESTS=1\n\nech"
},
{
"path": ".buildkite/linux/release-entrypoint",
"chars": 341,
"preview": "#!/bin/bash\n\nset -e\n\nexport PATH=\"$PATH:/state/rust/cargo/bin\"\nexport BUILD_ROOT=/state/build\nexport RELEASE=2\nln -sf ou"
},
{
"path": ".buildkite/mac/entrypoint",
"chars": 200,
"preview": "#!/bin/bash\n\nset -e\n\nSTATE=$(pwd)/../state/anki-ci\nmkdir -p $STATE\n\necho \"+++ Building and testing\"\nln -sf out/node_modu"
},
{
"path": ".buildkite/windows/entrypoint.bat",
"chars": 344,
"preview": "set PATH=c:\\cargo\\bin;%PATH%\n\necho +++ Building and testing\n\nif exist \\buildkite\\state\\out (\n move \\buildkite\\state\\o"
},
{
"path": ".cargo/config.toml",
"chars": 600,
"preview": "[env]\nSTRINGS_PY = { value = \"out/pylib/anki/_fluent.py\", relative = true }\nSTRINGS_TS = { value = \"out/ts/lib/generated"
},
{
"path": ".config/nextest.toml",
"chars": 34,
"preview": "[store]\ndir = \"out/tests/nextest\"\n"
},
{
"path": ".cursor/rules/building.md",
"chars": 148,
"preview": "- To build and check the project, use ./check in the root folder (or check.bat on Windows)\n- This will format files, the"
},
{
"path": ".cursor/rules/i18n.md",
"chars": 768,
"preview": "- We use the fluent system+code generation for translation.\n- New strings should be added to rslib/core/. Ask for the ap"
},
{
"path": ".deny.toml",
"chars": 2128,
"preview": "# all-features = true\n# features = []\n\n[advisories]\ndb-path = \"~/.cargo/advisory-db\"\ndb-urls = [\"https://github.com/rust"
},
{
"path": ".dockerignore",
"chars": 26,
"preview": "node_modules/\ntarget/\nout/"
},
{
"path": ".dprint.json",
"chars": 955,
"preview": "{\n \"typescript\": {\n \"indentWidth\": 4,\n \"useBraces\": \"always\"\n },\n \"json\": {\n \"indentWidth\""
},
{
"path": ".eslintrc.cjs",
"chars": 1982,
"preview": "module.exports = {\n root: true,\n extends: [\"eslint:recommended\", \"plugin:compat/recommended\", \"plugin:svelte/recom"
},
{
"path": ".gitattributes",
"chars": 79,
"preview": "* text=auto eol=lf\n*.ftl -linguist-detectable\ncargo/remote/* linguist-vendored\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report.md",
"chars": 815,
"preview": "---\nname: Developer Tasks\nabout: For bug reports, suggestions and support, please see the options below.\ntitle: \"\"\nlabel"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 576,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: Bug Reports\n url: https://forums.ankiweb.net\n about: Th"
},
{
"path": ".github/actions/setup-anki/action.yml",
"chars": 3151,
"preview": "name: Setup Anki Build Environment\ndescription: Install system dependencies, Rust toolchain, uv, and n2\n\nruns:\n using: "
},
{
"path": ".github/workflows/ci.yml",
"chars": 7460,
"preview": "name: CI\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n types: [opened, synchronize, reopene"
},
{
"path": ".gitignore",
"chars": 254,
"preview": "__pycache__\n.mypy_cache\n.DS_Store\nanki.prof\ntarget\n/user.bazelrc\n.dmypy.json\n/.idea/\n/.vscode\n/.bazel\n/windows.bazelrc\n/"
},
{
"path": ".gitmodules",
"chars": 240,
"preview": "[submodule \"ftl/core-repo\"]\n\tpath = ftl/core-repo\n\turl = https://github.com/ankitects/anki-core-i18n.git\n\tshallow = true"
},
{
"path": ".idea.dist/repo.iml",
"chars": 604,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"PYTHON_MODULE\" version=\"4\">\n <component name=\"NewModuleRootManager"
},
{
"path": ".mypy.ini",
"chars": 3738,
"preview": "[mypy]\npython_version = 3.9\npretty = False\nstrict_optional = False\nshow_error_codes = True\ncheck_untyped_defs = True\ndis"
},
{
"path": ".prettierrc",
"chars": 176,
"preview": "{\n \"trailingComma\": \"all\",\n \"printWidth\": 88,\n \"tabWidth\": 4,\n \"semi\": true,\n \"htmlWhitespaceSensitivity\""
},
{
"path": ".python-version",
"chars": 7,
"preview": "3.13.5\n"
},
{
"path": ".ruff.toml",
"chars": 2685,
"preview": "lint.select = [\n \"E\", # pycodestyle errors\n \"F\", # Pyflakes errors\n \"PL\", # Pylint rules\n \"I\", # Isort rules\n \"ARG\""
},
{
"path": ".rustfmt-empty.toml",
"chars": 0,
"preview": ""
},
{
"path": ".rustfmt.toml",
"chars": 259,
"preview": "# These settings are not supported on stable Rust, and are ignored by the ninja\n# build script - to use them you need to"
},
{
"path": ".version",
"chars": 8,
"preview": "25.09.2\n"
},
{
"path": ".vscode.dist/extensions.json",
"chars": 275,
"preview": "{\n \"recommendations\": [\n \"dprint.dprint\",\n \"ms-python.python\",\n \"charliermarsh.ruff\",\n \"r"
},
{
"path": ".vscode.dist/launch.json",
"chars": 1354,
"preview": "{\n // Use IntelliSense to learn about possible attributes.\n // Hover to view descriptions of existing attributes.\n"
},
{
"path": ".vscode.dist/settings.json",
"chars": 1354,
"preview": "{\n \"editor.formatOnSave\": true,\n \"[python]\": {\n \"editor.codeActionsOnSave\": {\n \"source.organizeI"
},
{
"path": ".vscode.dist/tasks.json",
"chars": 397,
"preview": "{\n \"version\": \"2.0.0\",\n \"tasks\": [\n {\n \"label\": \"ninja\",\n \"command\": \"ninja\",\n "
},
{
"path": ".yarnrc.yml",
"chars": 46,
"preview": "nodeLinker: node-modules\nenableScripts: false\n"
},
{
"path": "CLAUDE.md",
"chars": 3131,
"preview": "# Claude Code Configuration\n\n## Project Overview\n\nAnki is a spaced repetition flashcard program with a multi-layered arc"
},
{
"path": "CONTRIBUTORS",
"chars": 12307,
"preview": "If you have made changes to Anki's AGPL code, you are welcome to distribute\nthe changed code under the AGPL license.\n\nIf"
},
{
"path": "Cargo.toml",
"chars": 5346,
"preview": "[workspace.package]\nversion = \"0.0.0\"\nauthors = [\"Ankitects Pty Ltd and contributors <https://help.ankiweb.net>\"]\neditio"
},
{
"path": "LICENSE",
"chars": 1796,
"preview": "Anki is licensed under the GNU Affero General Public License, version 3 or\nlater, with portions contributed by Anki user"
},
{
"path": "README.md",
"chars": 888,
"preview": "# Anki®\n\n[.\n\n## Architecture\n\nThe bu"
},
{
"path": "docs/contributing.md",
"chars": 4943,
"preview": "# Contributing Code\n\nFor info on contributing things other than code, such as translations, decks\nand add-ons, please se"
},
{
"path": "docs/development.md",
"chars": 8174,
"preview": "# Anki development\n\n## Packaged betas\n\nFor non-developers who want to try beta versions, the easiest way is to use a\npac"
},
{
"path": "docs/docker/Dockerfile",
"chars": 2554,
"preview": "# This is a user-contributed Dockerfile. No official support is available.\n\nARG DEBIAN_FRONTEND=\"noninteractive\"\n\nFROM u"
},
{
"path": "docs/docker/README.md",
"chars": 3558,
"preview": "# Building and running Anki in Docker\n\nThis is an example Dockerfile contributed by an Anki user, which shows how Anki\nc"
},
{
"path": "docs/editing.md",
"chars": 1938,
"preview": "# Editing/IDEs\n\nVisual Studio Code is recommended, since it provides decent support for all the languages\nAnki uses. To "
},
{
"path": "docs/language_bridge.md",
"chars": 6438,
"preview": "Anki's codebase uses three layers.\n\n1. The web frontend, created in Svelte and typescript,\n2. The Python layer and\n3. Th"
},
{
"path": "docs/linux.md",
"chars": 4195,
"preview": "# Linux-specific notes\n\n## Requirements\n\nThese instructions are written for Debian/Ubuntu; adjust for your distribution."
},
{
"path": "docs/mac.md",
"chars": 383,
"preview": "# Mac-specific notes\n\n## Requirements\n\n**Xcode**:\n\nInstall the latest XCode from the App Store. Open it at least once\nso"
},
{
"path": "docs/ninja.md",
"chars": 722,
"preview": "Brief notes for people used to the existing Bazel build system:\n\n- Put the ninja binary on your path: https://github.com"
},
{
"path": "docs/protobuf.md",
"chars": 4292,
"preview": "ProtoBuf is a format used both to save data in storage and transmit\ndata between services. You can think of it as simila"
},
{
"path": "docs/syncserver/Dockerfile",
"chars": 1139,
"preview": "FROM rust:1.85.0-alpine3.20 AS builder\n\nARG ANKI_VERSION\n\nRUN apk update && apk add --no-cache build-base protobuf && rm"
},
{
"path": "docs/syncserver/Dockerfile.distroless",
"chars": 987,
"preview": "FROM rust:1.85.0 AS builder\n\nARG ANKI_VERSION\n\nRUN apt-get update && apt-get install -y build-essential protobuf-compile"
},
{
"path": "docs/syncserver/README.md",
"chars": 5313,
"preview": "# Building and running Anki sync server in Docker\n\nThis is an example Dockerfile contributed by an Anki user, which show"
},
{
"path": "docs/syncserver/entrypoint.sh",
"chars": 723,
"preview": "#!/bin/sh\nset -o errexit\nset -o nounset\nset -o pipefail\n\n# Default PUID and PGID if not provided\nexport PUID=${PUID:-100"
},
{
"path": "docs/windows.md",
"chars": 1543,
"preview": "# Windows\n\n## Requirements\n\n**Windows**:\n\nYou must be running 64 bit Windows 10, version 1703 or newer.\n\n**Rustup**:\n\nAs"
},
{
"path": "ftl/.gitignore",
"chars": 45,
"preview": "usage/*\n!usage/no-deprecate.json\nmobile-repo\n"
},
{
"path": "ftl/Cargo.toml",
"chars": 514,
"preview": "[package]\nname = \"ftl\"\nversion.workspace = true\nauthors.workspace = true\nedition.workspace = true\nlicense.workspace = tr"
},
{
"path": "ftl/README.md",
"chars": 98,
"preview": "Files related to Anki's translations.\n\nPlease see https://translating.ankiweb.net/anki/developers\n"
},
{
"path": "ftl/copy-core-string.sh",
"chars": 344,
"preview": "#!/bin/bash\n# - sync ftl\n# - ./copy-core-string.sh scheduling-review browsing-sidebar-card-state-review\n# - confirm chan"
},
{
"path": "ftl/core/actions.ftl",
"chars": 3586,
"preview": "actions-add = Add\n# Action in context menu:\n# In the browser sidebar, when in \"Select\" mode, right-click on the\n# select"
},
{
"path": "ftl/core/adding.ftl",
"chars": 710,
"preview": "adding-add-shortcut-ctrlandenter = Add (shortcut: ctrl+enter)\nadding-added = Added\nadding-discard-current-input = Discar"
},
{
"path": "ftl/core/browsing.ftl",
"chars": 7430,
"preview": "browsing-add-notes = Add Notes...\nbrowsing-add-tags2 = Add Tags...\nbrowsing-add-to-selected-notes = Add to Selected Note"
},
{
"path": "ftl/core/card-stats.ftl",
"chars": 1861,
"preview": "card-stats-added = Added\ncard-stats-first-review = First Review\ncard-stats-latest-review = Latest Review\ncard-stats-inte"
},
{
"path": "ftl/core/card-template-rendering.ftl",
"chars": 2005,
"preview": "### These messages are shown on the review screen, preview screen, and\n### card template screen when the user has made a"
},
{
"path": "ftl/core/card-templates.ftl",
"chars": 3703,
"preview": "# This word is used by TTS voices instead of the elided part of a cloze.\ncard-templates-blank = blank\ncard-templates-cha"
},
{
"path": "ftl/core/change-notetype.ftl",
"chars": 648,
"preview": "change-notetype-current = Current\nchange-notetype-new = New\nchange-notetype-nothing = (Nothing)\nchange-notetype-collapse"
},
{
"path": "ftl/core/custom-study.ftl",
"chars": 2787,
"preview": "### options related to the Custom Study window\ncustom-study-increase-todays-new-card-limit = Increase today's new card l"
},
{
"path": "ftl/core/database-check.ftl",
"chars": 2876,
"preview": "database-check-corrupt = Collection file is corrupt. Please restore from an automatic backup.\ndatabase-check-rebuilt = D"
},
{
"path": "ftl/core/deck-config.ftl",
"chars": 37630,
"preview": "### Text shown on the \"Deck Options\" screen\n\n## Top section\n\n# Used in the deck configuration screen to show how many de"
},
{
"path": "ftl/core/decks.ftl",
"chars": 2888,
"preview": "## In the options window of a filtered deck\ndecks-limit-to = Limit to\ndecks-cards-selected-by = cards selected by\ndecks-"
},
{
"path": "ftl/core/editing.ftl",
"chars": 4779,
"preview": "editing-actual-size = Toggle actual size\nediting-add-media = Add Media\nediting-align-left = Align left\nediting-align-rig"
},
{
"path": "ftl/core/empty-cards.ftl",
"chars": 604,
"preview": "empty-cards-for-note-type = Empty cards for { $notetype }:\nempty-cards-count-line = { $empty_count } of { $existing_coun"
},
{
"path": "ftl/core/errors.ftl",
"chars": 1370,
"preview": "errors-parse-number-fail = A number was invalid or out of range.\nerrors-filtered-parent-deck = Filtered decks can not ha"
},
{
"path": "ftl/core/exporting.ftl",
"chars": 1893,
"preview": "exporting-all-decks = All Decks\nexporting-anki-20-deck = Anki 2.0 Deck\nexporting-anki-collection-package = Anki Collecti"
},
{
"path": "ftl/core/fields.ftl",
"chars": 1277,
"preview": "fields-add-field = Add Field\nfields-delete-field-from = Delete field from { $val }?\nfields-editing-font = Editing Font\nf"
},
{
"path": "ftl/core/findreplace.ftl",
"chars": 161,
"preview": "findreplace-notes-updated =\n { $total ->\n [one] { $changed } of { $total } note updated\n *[other] { $cha"
},
{
"path": "ftl/core/help.ftl",
"chars": 504,
"preview": "### Text shown in Help pages\n\n## Header/footer\n\n# Link to more detailed information in the manual\nhelp-for-more-info = F"
},
{
"path": "ftl/core/importing.ftl",
"chars": 13692,
"preview": "importing-failed-debug-info = Import failed. Debugging info:\nimporting-aborted = Aborted: { $val }\nimporting-added-dupli"
},
{
"path": "ftl/core/keyboard.ftl",
"chars": 44,
"preview": "keyboard-ctrl = Ctrl\nkeyboard-shift = Shift\n"
},
{
"path": "ftl/core/launcher.ftl",
"chars": 2154,
"preview": "launcher-title = Anki Launcher\nlauncher-press-enter-to-install = Press the Enter/Return key on your keyboard to install "
},
{
"path": "ftl/core/media-check.ftl",
"chars": 3031,
"preview": "## Shown at the top of the media check screen\n\nmedia-check-window-title = Check Media\n# the number of files, and the tot"
},
{
"path": "ftl/core/media.ftl",
"chars": 627,
"preview": "media-error-executing = Error executing { $val }.\nmedia-error-running = Error running { $val }\nmedia-for-security-reason"
},
{
"path": "ftl/core/network.ftl",
"chars": 333,
"preview": "network-offline = Please check your internet connection.\nnetwork-timeout = Connection timed out. Please try again. If yo"
},
{
"path": "ftl/core/notetypes.ftl",
"chars": 2280,
"preview": "notetypes-notetype = Note Type\n\n## Default field names in newly created note types\n\nnotetypes-front-field = Front\nnotety"
},
{
"path": "ftl/core/preferences.ftl",
"chars": 6237,
"preview": "preferences-automatically-sync-on-profile-openclose = Automatically sync on profile open/close\npreferences-backups = Bac"
},
{
"path": "ftl/core/profiles.ftl",
"chars": 1540,
"preview": "profiles-anki-could-not-read-your-profile = Anki could not read your profile data. Window sizes and your sync login deta"
},
{
"path": "ftl/core/scheduling.ftl",
"chars": 7486,
"preview": "## The next time a card will be shown, in a short form that will fit\n## on the answer buttons. For example, English show"
},
{
"path": "ftl/core/search.ftl",
"chars": 3297,
"preview": "## Errors shown when invalid search input is encountered.\n## Backticks change the text formatting, so please don't chang"
},
{
"path": "ftl/core/statistics.ftl",
"chars": 12364,
"preview": "# The date a card will be ready to review\nstatistics-due-date = Due\n# The count of cards waiting to be reviewed\nstatisti"
},
{
"path": "ftl/core/studying.ftl",
"chars": 2747,
"preview": "studying-again = Again\nstudying-all-buried-cards = All Buried Cards\nstudying-audio-5s = Audio -5s\nstudying-audio-and5s ="
},
{
"path": "ftl/core/sync.ftl",
"chars": 4335,
"preview": "### Messages shown when synchronizing with AnkiWeb.\n\n\n## Media synchronization\n\nsync-media-added-count = Added: { $up }↑"
},
{
"path": "ftl/core/undo.ftl",
"chars": 348,
"preview": "### The strings in this file are currently in development,\n### and you may want to skip translating them for now.\n\nundo-"
},
{
"path": "ftl/ftl",
"chars": 56,
"preview": "#!/bin/bash\n\ncd $(dirname $0)/..\ncargo run -p ftl -- $*\n"
},
{
"path": "ftl/move-from-ankimobile",
"chars": 170,
"preview": "#!/bin/bash\n#\n# Move a translation that previously only existed in AnkiMobile to the core translations.\n#\n\n./ftl string "
},
{
"path": "ftl/qt/about.ftl",
"chars": 964,
"preview": "about-a-big-thanks-to-all-the = A big thanks to all the people who have provided suggestions, bug reports and donations."
},
{
"path": "ftl/qt/addons.ftl",
"chars": 5050,
"preview": "addons-possibly-involved = Add-ons possibly involved: { $addons }\naddons-failed-to-load =\n An add-on you installed fa"
},
{
"path": "ftl/qt/errors.ftl",
"chars": 2306,
"preview": "-errors-support-site = [support site](https://help.ankiweb.net)\nerrors-standard-popup2 =\n Anki encountered a problem."
},
{
"path": "ftl/qt/preferences.ftl",
"chars": 508,
"preview": "## Video drivers/hardware acceleration. Please avoid translating 'OpenGL' and 'ANGLE'.\n\npreferences-video-driver = Video"
},
{
"path": "ftl/qt/profiles.ftl",
"chars": 279,
"preview": "profiles-folder-readme =\n This folder stores all of your Anki data in a single location,\n to make backups easy. To"
},
{
"path": "ftl/qt/qt-accel.ftl",
"chars": 1590,
"preview": "qt-accel-about = &About\nqt-accel-about-mac = About Anki...\nqt-accel-cards = &Cards\nqt-accel-check-database = &Check Data"
},
{
"path": "ftl/qt/qt-misc.ftl",
"chars": 6360,
"preview": "qt-misc-addon-will-be-installed-when-a = Add-on will be installed when a profile is opened.\nqt-misc-addons = Add-ons\nqt-"
},
{
"path": "ftl/remove-unused.sh",
"chars": 888,
"preview": "#!/bin/bash\n#\n# To use, run:\n#\n# - ./update-ankimobile-usage.sh\n# - ./remove-unused.sh\n#\n# If you need to maintain compa"
},
{
"path": "ftl/src/garbage_collection.rs",
"chars": 8977,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/main.rs",
"chars": 1985,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/serialize.rs",
"chars": 13317,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/string/copy.rs",
"chars": 3303,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/string/mod.rs",
"chars": 4752,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/string/transform.rs",
"chars": 6838,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/src/sync.rs",
"chars": 3012,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "ftl/update-ankidroid-usage.sh",
"chars": 96,
"preview": "#!/bin/bash\n\ncargo run --bin write_ftl_json ftl/usage/ankidroid.json ~/Local/droid/Anki-Android\n"
},
{
"path": "ftl/update-ankimobile-usage.sh",
"chars": 186,
"preview": "#!/bin/bash\n# This script can only be run by Damien, as it requires a copy of AnkiMobile's sources.\n\ncargo run --bin wri"
},
{
"path": "ftl/usage/no-deprecate.json",
"chars": 71,
"preview": "[\n \"scheduling-update-soon\",\n \"scheduling-update-later-button\"\n]\n"
},
{
"path": "justfile",
"chars": 965,
"preview": "set windows-shell := [\"cmd.exe\", \"/c\"]\n\n# Show available commands\ndefault:\n @just --list\n\n# Run all tests (Rust, Pyth"
},
{
"path": "ninja",
"chars": 364,
"preview": "#!/bin/bash\n\nset -e\n\nif [ \"$BUILD_ROOT\" == \"\" ]; then\n out=$(pwd)/out\nelse\n out=\"$BUILD_ROOT\"\nfi\nexport CARGO_TARG"
},
{
"path": "package.json",
"chars": 3306,
"preview": "{\n \"name\": \"anki\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"author\": \"Ankitects Pty Ltd and contributors\",\n "
},
{
"path": "pkgkey.asc",
"chars": 4447,
"preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFueX68BEAClpx+Szt1cSTWJTCTpn9E+tGhYUKVpj1O4KGAj7qYKs651LPOA\nen1Ng0MoK4Avq4zW3"
},
{
"path": "proto/.clang-format",
"chars": 21,
"preview": "BasedOnStyle: google\n"
},
{
"path": "proto/.top_level",
"chars": 0,
"preview": ""
},
{
"path": "proto/README.md",
"chars": 237,
"preview": "Protobuf files defining the interface the frontend and backend components use to talk to each other,\nand how Anki stores"
},
{
"path": "proto/anki/ankidroid.proto",
"chars": 2199,
"preview": "syntax = \"proto3\";\n\noption java_multiple_files = true;\n\nimport \"anki/generic.proto\";\nimport \"anki/scheduler.proto\";\n\npac"
},
{
"path": "proto/anki/ankihub.proto",
"chars": 578,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/ankiweb.proto",
"chars": 1227,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/backend.proto",
"chars": 1571,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/card_rendering.proto",
"chars": 4894,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/cards.proto",
"chars": 1827,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/collection.proto",
"chars": 4477,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/config.proto",
"chars": 3808,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/deck_config.proto",
"chars": 8389,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/decks.proto",
"chars": 5979,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/frontend.proto",
"chars": 1371,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/generic.proto",
"chars": 497,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/i18n.proto",
"chars": 1257,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/image_occlusion.proto",
"chars": 2716,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/import_export.proto",
"chars": 6156,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/links.proto",
"chars": 1118,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/media.proto",
"chars": 1146,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/notes.proto",
"chars": 2898,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/notetypes.proto",
"chars": 6886,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/scheduler.proto",
"chars": 12735,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/search.proto",
"chars": 4638,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/stats.proto",
"chars": 5850,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/sync.proto",
"chars": 2349,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "proto/anki/tags.proto",
"chars": 2063,
"preview": "// Copyright: Ankitects Pty Ltd and contributors\n// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/a"
},
{
"path": "pylib/.gitignore",
"chars": 109,
"preview": "*.mo\n*.pyc\n*\\#\n*~\n.*.swp\n.build\n.coverage\n.DS_Store\n.mypy_cache\n.pytype\n__pycache__\nanki.egg-info\nbuild\ndist\n"
},
{
"path": "pylib/README.md",
"chars": 138,
"preview": "Anki's Python library code is in anki/.\n\nThe Rust/Python extension module is in rsbridge/; it references the library def"
},
{
"path": "pylib/anki/_backend.py",
"chars": 8755,
"preview": "# Copyright: Ankitects Pty Ltd and contributors\n# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agp"
},
{
"path": "pylib/anki/_legacy.py",
"chars": 7442,
"preview": "# Copyright: Ankitects Pty Ltd and contributors\n# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agp"
},
{
"path": "pylib/anki/_rsbridge.pyi",
"chars": 357,
"preview": "from typing import Union\n\nclass Backend:\n @classmethod\n def command(cls, service: int, method: int, data: bytes) -"
},
{
"path": "pylib/anki/_vendor/stringcase.py",
"chars": 4784,
"preview": "# stringcase 1.2.0 with python warning fix applied\n# MIT: https://github.com/okunishinishi/python-stringcase\n\n\n\"\"\"\nStrin"
},
{
"path": "pylib/anki/browser.py",
"chars": 1205,
"preview": "# Copyright: Ankitects Pty Ltd and contributors\n# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agp"
}
]
// ... and 1395 more files (download for full content)
About this extraction
This page contains the full source code of the ankitects/anki GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1595 files (6.2 MB), approximately 1.7M tokens, and a symbol index with 9270 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.