├── .github └── workflows │ ├── build.sh │ └── gh-pages.yml ├── .gitignore ├── .nojekyll ├── CNAME ├── README.md ├── book.toml └── src ├── SUMMARY.md ├── a-basic-addon.md ├── addon-config.md ├── addon-folders.md ├── background-ops.md ├── command-line-use.md ├── console-output.md ├── debugging.md ├── editing-and-mypy.md ├── editor-setup.md ├── hooks-and-filters.md ├── img └── autocomplete.mp4 ├── intro.md ├── monkey-patching.md ├── mypy.md ├── porting2.0.md ├── porting2.1.x.md ├── python-modules.md ├── qt.md ├── reviewer-javascript.md ├── sharing.md ├── support.md └── the-anki-module.md /.github/workflows/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Version configuration 4 | MDBOOK_VERSION="0.4.51" 5 | MDBOOK_LINKCHECK_COMMIT="3222e857f22a5b41596070848203d160a4d78fd9" 6 | MDBOOK_TOC_VERSION="0.14.2" 7 | 8 | # mdbook needs to be able to find things like mdbook-toc 9 | export PATH="$HOME/.cargo/bin:$PATH" 10 | 11 | install_if_needed() { 12 | local binary=$1 13 | local install_cmd=$2 14 | 15 | if [ "$GITHUB_ACTIONS" = "true" ]; then 16 | # avoid recompiling if cached 17 | if ! command -v $binary &> /dev/null; then 18 | eval $install_cmd 19 | fi 20 | else 21 | eval $install_cmd 22 | fi 23 | } 24 | 25 | # Install required binaries 26 | install_if_needed "mdbook" "cargo install mdbook --version $MDBOOK_VERSION" 27 | install_if_needed "mdbook-toc" "cargo install mdbook-toc --version $MDBOOK_TOC_VERSION" 28 | install_if_needed "mdbook-linkcheck" "cargo install --git https://github.com/ankitects/mdbook-linkcheck --rev $MDBOOK_LINKCHECK_COMMIT mdbook-linkcheck" 29 | 30 | # Avoid checking links in deploy mode 31 | if [ "$CHECK" == "" ]; then 32 | mv $HOME/.cargo/bin/mdbook-linkcheck linkcheck-tmp 33 | fi 34 | 35 | mdbook --version 36 | mdbook build 37 | 38 | if [ "$CHECK" == "" ]; then 39 | # restore linkcheck so it gets stored in the cache 40 | mv linkcheck-tmp $HOME/.cargo/bin/mdbook-linkcheck 41 | fi 42 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | env: 2 | cname: addon-docs.ankiweb.net 3 | 4 | name: github pages 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - test 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-24.04 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install Rust 19 | uses: dtolnay/rust-toolchain@stable 20 | with: 21 | components: rustfmt 22 | 23 | - name: Cache binaries 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/.cargo/bin 27 | key: ${{ runner.os }}-bin-${{ hashFiles('.github/workflows/build.sh') }} 28 | restore-keys: | 29 | ${{ runner.os }}-bin- 30 | 31 | - name: Build 32 | run: .github/workflows/build.sh 33 | 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@v4 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./book/html 39 | cname: ${{ env.cname }} 40 | 41 | check: 42 | runs-on: ubuntu-24.04 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - name: Install Rust 47 | uses: dtolnay/rust-toolchain@stable 48 | with: 49 | components: rustfmt 50 | 51 | - name: Cache binaries 52 | uses: actions/cache@v4 53 | with: 54 | path: ~/.cargo/bin 55 | key: ${{ runner.os }}-bin-${{ hashFiles('.github/workflows/build.sh') }} 56 | restore-keys: | 57 | ${{ runner.os }}-bin- 58 | 59 | - name: Build 60 | run: CHECK=1 .github/workflows/build.sh 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitects/addon-docs/369dae7a88fe1c9704e90acf97ce26c010436ece/.nojekyll -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | addon-docs.ankiweb.net -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Best viewed on the website: 2 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Ankitects Pty Ltd and contributors"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Writing Anki Add-ons" 7 | 8 | [build] 9 | create-missing = true 10 | 11 | [preprocessor.toc] 12 | command = "mdbook-toc" 13 | renderer = ["html"] 14 | 15 | [output.html] 16 | git-repository-url = "https://github.com/ankitects/addon-docs/" 17 | cname = "addon-docs.ankiweb.net" 18 | 19 | [output.linkcheck] 20 | optional = true 21 | warning-policy = "warn" 22 | follow-web-links = true 23 | exclude = [ 'forums.ankiweb.net' ] -------------------------------------------------------------------------------- /src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](intro.md) 4 | - [Support](support.md) 5 | - [Editor Setup](editor-setup.md) 6 | - [MyPy](mypy.md) 7 | - [Add-on Folders](addon-folders.md) 8 | - [A Basic Add-on](a-basic-addon.md) 9 | - [The 'anki' Module](the-anki-module.md) 10 | - [Command-Line Use](command-line-use.md) 11 | - [Hooks and Filters](hooks-and-filters.md) 12 | - [Console Output](console-output.md) 13 | - [Background Operations](background-ops.md) 14 | - [Qt and PyQt](qt.md) 15 | - [Python Modules](python-modules.md) 16 | - [Add-on Config](addon-config.md) 17 | - [Reviewer Javascript](reviewer-javascript.md) 18 | - [Debugging](debugging.md) 19 | - [Monkey Patching](monkey-patching.md) 20 | - [Sharing Add-ons](sharing.md) 21 | - [Porting 2.1.x Add-ons](porting2.1.x.md) 22 | - [Porting 2.0 Add-ons](porting2.0.md) 23 | -------------------------------------------------------------------------------- /src/a-basic-addon.md: -------------------------------------------------------------------------------- 1 | # A Basic Add-on 2 | 3 | Add the following to `myaddon/__init__.py` in your add-ons folder: 4 | 5 | ```python 6 | # import the main window object (mw) from aqt 7 | from aqt import mw 8 | # import the "show info" tool from utils.py 9 | from aqt.utils import showInfo, qconnect 10 | # import all of the Qt GUI library 11 | from aqt.qt import * 12 | 13 | # We're going to add a menu item below. First we want to create a function to 14 | # be called when the menu item is activated. 15 | 16 | def testFunction() -> None: 17 | # get the number of cards in the current collection, which is stored in 18 | # the main window 19 | cardCount = mw.col.card_count() 20 | # show a message box 21 | showInfo("Card count: %d" % cardCount) 22 | 23 | # create a new menu item, "test" 24 | action = QAction("test", mw) 25 | # set it to call testFunction when it's clicked 26 | qconnect(action.triggered, testFunction) 27 | # and add it to the tools menu 28 | mw.form.menuTools.addAction(action) 29 | ``` 30 | 31 | Restart Anki, and you should find a 'test' item in the tools menu. 32 | Running it will display a dialog with the card count. 33 | 34 | If you make a mistake when entering in the plugin, Anki will show an 35 | error message on startup indicating where the problem is. 36 | -------------------------------------------------------------------------------- /src/addon-config.md: -------------------------------------------------------------------------------- 1 | # Add-on Config 2 | 3 | ## Config JSON 4 | 5 | If you include a config.json file with a JSON dictionary in it, Anki 6 | will allow users to edit it from the add-on manager. 7 | 8 | A simple example: in config.json: 9 | 10 | {"myvar": 5} 11 | 12 | In config.md: 13 | 14 | This is documentation for this add-on's configuration, in *markdown* format. 15 | 16 | In your add-on’s code: 17 | 18 | ```python 19 | from aqt import mw 20 | config = mw.addonManager.getConfig(__name__) 21 | print("var is", config['myvar']) 22 | ``` 23 | 24 | When updating your add-on, you can make changes to config.json. Any 25 | newly added keys will be merged with the existing configuration. 26 | 27 | If you change the value of existing keys in config.json, users who have 28 | customized their configuration will continue to see the old values 29 | unless they use the "restore defaults" button. 30 | 31 | If you need to programmatically modify the config, you can save your 32 | changes with: 33 | 34 | ```python 35 | mw.addonManager.writeConfig(__name__, config) 36 | ``` 37 | 38 | If no config.json file exists, getConfig() will return None - even if 39 | you have called writeConfig(). 40 | 41 | Add-ons that manage options in their own GUI can have that GUI displayed 42 | when the config button is clicked: 43 | 44 | ```python 45 | mw.addonManager.setConfigAction(__name__, myOptionsFunc) 46 | ``` 47 | 48 | Avoid key names starting with an underscore - they are reserved for 49 | future use by Anki. 50 | 51 | ## User Files 52 | 53 | When your add-on needs configuration data other than simple keys and 54 | values, it can use a special folder called user_files in the root of 55 | your add-on’s folder. Any files placed in this folder will be preserved 56 | when the add-on is upgraded. All other files in the add-on folder are 57 | removed on upgrade. 58 | 59 | To ensure the user_files folder is created for the user, you can put a 60 | README.txt or similar file inside it before zipping up your add-on. 61 | 62 | When Anki upgrades an add-on, it will ignore any files in the .zip that 63 | already exist in the user_files folder. 64 | -------------------------------------------------------------------------------- /src/addon-folders.md: -------------------------------------------------------------------------------- 1 | # Add-on Folders 2 | 3 | You can access the top level add-ons folder by going to the 4 | Tools>Add-ons menu item in the main Anki window. Click on the View 5 | Files button, and a folder will pop up. If you had no add-ons installed, 6 | the top level add-ons folder will be shown. If you had an add-on 7 | selected, the add-on’s module folder will be shown, and you will need to 8 | go up one level. 9 | 10 | The add-ons folder is named "addons21", corresponding to Anki 2.1. If 11 | you have an "addons" folder, it is because you have previously used Anki 12 | 2.0.x. 13 | 14 | Each add-on uses one folder inside the add-on folder. Anki looks for a 15 | file called `__init__.py` file inside the folder, eg: 16 | 17 | addons21/myaddon/__init__.py 18 | 19 | If `__init__.py` does not exist, Anki will ignore the folder. 20 | 21 | When choosing a folder name, it is recommended to stick to a-z and 0-9 22 | characters to avoid problems with Python’s module system. 23 | 24 | While you can use whatever folder name you wish for folders you create 25 | yourself, when you download an add-on from AnkiWeb, Anki will use the 26 | item’s ID as the folder name, such as: 27 | 28 | addons21/48927303923/__init__.py 29 | 30 | Anki will also place a meta.json file in the folder, which keeps track 31 | of the original add-on name, when it was downloaded, and whether it’s 32 | enabled or not. 33 | 34 | You should not store user data in the add-on folder, as it’s [deleted 35 | when the user upgrades an add-on](addon-config.md#config-json). 36 | 37 | If you followed the steps in the [editor setup](editor-setup.md) section, you 38 | can either copy your myaddon folder into Anki’s add-on folder to test it, or on 39 | Mac or Linux, create a symlink from the folder’s original location into your 40 | add-ons folder. 41 | -------------------------------------------------------------------------------- /src/background-ops.md: -------------------------------------------------------------------------------- 1 | # Background Operations 2 | 3 | If your add-on performs a long-running operation directly, the user interface will freeze 4 | until the operation completes - no progress window will be shown, and the app will look as 5 | if it's stuck. This is annoying for users, so care should be taken to avoid it happening. 6 | 7 | The reason it happens is because the user interface runs on the "main thread". When your add-on 8 | performs a long-running operation directly, it also runs on the main thread, and it prevents 9 | the UI code from running again until your operation completes. The solution is to run your add-on 10 | code in a background thread, so that the UI can continue to function. 11 | 12 | A complicating factor is that any code you write that interacts with the UI also needs to be 13 | run on the main thread. If your add-on only ran in the background, and it attempted to access the 14 | UI, it would cause Anki to crash. So selectivity is required - UI operations should be run on 15 | the main thread, and long-running operations like collection and network access should be run in 16 | the background. Anki provides some tools to make this easier. 17 | 18 | ## Read-Only/Non-Undoable Operations 19 | 20 | For long-running operations like gathering a group of notes, or things like network access, 21 | `QueryOp` is recommended. For the latter, make sure to read about serialization further below. 22 | 23 | In the following example, my_ui_action() will return quickly, and the operation 24 | will continue to run in the background until it completes. If it finishes 25 | successfully, on_success will be called. 26 | 27 | ```python 28 | from anki.collection import Collection 29 | from aqt.operations import QueryOp 30 | from aqt.utils import showInfo 31 | from aqt import mw 32 | 33 | def my_background_op(col: Collection, note_ids: list[int]) -> int: 34 | # some long-running op, eg 35 | for id in note_ids: 36 | note = col.get_note(note_id) 37 | # ... 38 | 39 | return 123 40 | 41 | def on_success(count: int) -> None: 42 | showInfo(f"my_background_op() returned {count}") 43 | 44 | def my_ui_action(note_ids: list[int]): 45 | op = QueryOp( 46 | # the active window (main window in this case) 47 | parent=mw, 48 | # the operation is passed the collection for convenience; you can 49 | # ignore it if you wish 50 | op=lambda col: my_background_op(col, note_ids), 51 | # this function will be called if op completes successfully, 52 | # and it is given the return value of the op 53 | success=on_success, 54 | ) 55 | 56 | # if with_progress() is not called, no progress window will be shown. 57 | # note: QueryOp.with_progress() was broken until Anki 2.1.50 58 | op.with_progress().run_in_background() 59 | ``` 60 | 61 | **Be careful not to directly call any Qt/UI routines inside the background operation!** 62 | 63 | - If you need to modify the UI after an operation completes (e.g.show a tooltip), 64 | you should do it from the success function. 65 | - If the operation needs data from the UI (e.g.a combo box value), that data should be gathered 66 | prior to executing the operation. 67 | - If you need to update the UI during the background operation (e.g.to update the text of the 68 | progress window), your operation needs to perform that update on the main thread. For example, 69 | in a loop: 70 | 71 | ```python 72 | if time.time() - last_progress >= 0.1: 73 | aqt.mw.taskman.run_on_main( 74 | lambda: aqt.mw.progress.update( 75 | label=f"Remaining: {remaining}", 76 | value=total - remaining, 77 | max=total, 78 | ) 79 | ) 80 | last_progress = time.time() 81 | ``` 82 | 83 | **Operations are serialized by default** 84 | 85 | By default, only a single operation can run at once, to ensure multiple read operations on the 86 | collection don't interleave with another write operation. 87 | 88 | If your operation does not touch the collection (e.g., it is a network request), then you can 89 | opt out of this serialization so that the operation runs concurrently to other ops: 90 | 91 | ```python 92 | op.without_collection().run_in_background() 93 | ``` 94 | 95 | ## Collection Operations 96 | 97 | A separate `CollectionOp` is provided for undoable operations that modify 98 | the collection. It functions similarly to QueryOp, but will also update the 99 | UI as changes are made (e.g.refresh the Browse screen if any notes are changed). 100 | 101 | Many undoable ops already have a `CollectionOp` defined in [aqt/operations/\*.py](https://github.com/ankitects/anki/tree/main/qt/aqt/operations). 102 | You can often use one of them directly rather than having to create your own. 103 | For example: 104 | 105 | ```python 106 | from aqt.operations.note import remove_notes 107 | 108 | def my_ui_action(note_ids: list[int]) -> None: 109 | remove_notes(parent=mw, note_ids=note_ids).run_in_background() 110 | ``` 111 | 112 | By default that routine will show a tooltip on success. You can call .success() 113 | or .failure() on it to provide an alternative routine. 114 | 115 | For more information on undo handling, including combining multiple operations 116 | into a single undo step, please see [this forum 117 | page](https://forums.ankiweb.net/t/add-on-porting-notes-for-anki-2-1-45/11212#undoredo-4). 118 | -------------------------------------------------------------------------------- /src/command-line-use.md: -------------------------------------------------------------------------------- 1 | # Command-Line Use 2 | 3 | The `anki` module can be used separately from Anki's GUI. It is 4 | strongly recommended you use it instead of attempting to read or 5 | write a .anki2 file directly. 6 | 7 | Install it with pip: 8 | 9 | ```shell 10 | $ pip install anki 11 | ``` 12 | 13 | Then you can use it in a .py file, like so: 14 | 15 | ```python 16 | from anki.collection import Collection 17 | col = Collection("/path/to/collection.anki2") 18 | print(col.sched.deck_due_tree()) 19 | ``` 20 | 21 | See [the Anki module](./the-anki-module.md) for more. 22 | -------------------------------------------------------------------------------- /src/console-output.md: -------------------------------------------------------------------------------- 1 | # Console Output 2 | 3 | Because Anki is a GUI app, text output to stdout (e.g.`print("foo")`) is not 4 | usually visible to the user. You can optionally reveal text printed to stdout, 5 | and it is recommended that you do so while developing your add-on. 6 | 7 | ## Warnings 8 | 9 | Anki uses stdout to print warnings about API deprecations, eg: 10 | 11 | ``` 12 | addons21/mytest/__init__.py:10:getNote is deprecated: please use 'get_note' 13 | ``` 14 | 15 | If these warnings are occurring in a loop, please address them promptly, as they can 16 | slow Anki down even if the console is not shown. 17 | 18 | ## Printing text 19 | 20 | You may find it useful to print text to stdout to aid in debugging your add-on. 21 | Please avoid printing large amounts of text (e.g.in a loop that deals with hundreds or 22 | thousands of items), as that may slow Anki down, even if the console is not shown. 23 | 24 | ## Showing the Console 25 | 26 | ### Windows 27 | 28 | If you start Anki via the `anki-console.bat` file in `C:\Users\user\AppData\Local\Programs\Anki` (or `C:\Program Files\Anki`), a 29 | separate console window will appear. 30 | 31 | ### macOS 32 | 33 | Open Terminal.app, then enter the following text and hit enter: 34 | 35 | ``` 36 | /Applications/Anki.app/Contents/MacOS/anki 37 | ``` 38 | 39 | ### Linux 40 | 41 | Open a terminal/xterm, then run Anki with `anki` 42 | -------------------------------------------------------------------------------- /src/debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging 2 | 3 | ## Exceptions and Stdout/Stderr 4 | 5 | If your code throws an uncaught exception, it will be caught by Anki’s standard 6 | exception handler, and an error will be presented to the user. 7 | 8 | The handler catches anything that is printed to stderr, so you should avoid logging text 9 | to stderr unless you want the user to see it in a popup. 10 | 11 | Text printed to standard output is covered in [this section](./console-output.md). 12 | 13 | ## Webviews 14 | 15 | If you set the env var QTWEBENGINE_REMOTE_DEBUGGING to 8080 prior to starting Anki, 16 | you can surf to http://localhost:8080 in Chrome to debug the visible webpages. 17 | 18 | Alternatively, you can use [this add-on](https://ankiweb.net/shared/info/31746032) 19 | to open the inspector inside Anki. 20 | 21 | ## Debug Console 22 | 23 | Anki also includes a REPL. From within the program, press the [shortcut 24 | key](https://docs.ankiweb.net/misc.html#debug-console) and a 25 | window will open up. You can enter expressions or statements into the 26 | top area, and then press ctrl+return/command+return to evaluate them. An 27 | example session follows: 28 | 29 | >>> mw 30 | 31 | 32 | >>> print(mw) 33 | 34 | 35 | >>> invalidName 36 | Traceback (most recent call last): 37 | File "/Users/dae/Lib/anki/qt/aqt/main.py", line 933, in onDebugRet 38 | exec text 39 | File "", line 1, in 40 | NameError: name 'invalidName' is not defined 41 | 42 | >>> a = [a for a in dir(mw.form) if a.startswith("action")] 43 | ... print(a) 44 | ... print() 45 | ... pp(a) 46 | ['actionAbout', 'actionCheckMediaDatabase', ...] 47 | 48 | ['actionAbout', 49 | 'actionCheckMediaDatabase', 50 | 'actionDocumentation', 51 | 'actionDonate', 52 | ...] 53 | 54 | >>> pp(mw.reviewer.card) 55 | 56 | 57 | >>> pp(card()) # shortcut for mw.reviewer.card.__dict__ 58 | {'_note': , 59 | '_qa': [...] 60 | 'col': , 61 | 'data': u'', 62 | 'did': 1, 63 | 'due': -1, 64 | 'factor': 2350, 65 | 'flags': 0, 66 | 'id': 1307820012852L, 67 | [...] 68 | } 69 | 70 | >>> pp(bcard()) # shortcut for selected card in browser 71 | 72 | 73 | Note that you need to explicitly print an expression in order to see 74 | what it evaluates to. Anki exports pp() (pretty print) in the scope to 75 | make it easier to quickly dump the details of objects, and the shortcut 76 | ctrl+shift+return will wrap the current text in the upper area with pp() 77 | and execute the result. 78 | 79 | ## PDB 80 | 81 | If you’re on Linux or are running Anki from source, it’s also possible 82 | to debug your script with pdb. Place the following line somewhere in 83 | your code, and when Anki reaches that point it will kick into the 84 | debugger in the terminal: 85 | 86 | ```python 87 | from aqt.qt import debug; debug() 88 | ``` 89 | 90 | Alternatively you can export DEBUG=1 in your shell and it will kick into 91 | the debugger on an uncaught exception. 92 | 93 | ## Python Assertions 94 | 95 | Runtime checks using Python's `assert` statement are not evaluated in 96 | Anki's release builds, even when running in debug mode. If you want to 97 | use `assert` for testing you can use the [packaged versions from PyPI](https://betas.ankiweb.net/#via-pypipip) 98 | or [run Anki from source](https://github.com/ankitects/anki/blob/main/docs/development.md). 99 | -------------------------------------------------------------------------------- /src/editing-and-mypy.md: -------------------------------------------------------------------------------- 1 | # Editing and MyPy 2 | 3 | ## Editor/IDE setup 4 | 5 | The free community edition of PyCharm has good out of the box support 6 | for Python: . You can also use other 7 | editors like Visual Studio Code, but the instructions in this section 8 | will cover PyCharm. 9 | 10 | Over the last year, Anki’s codebase has been updated to add type hints to almost 11 | all of the code. These type hints make development easier, by providing better 12 | code completion, and by catching errors using tools like mypy. As an add-on 13 | author, you can take advantage of this type hinting as well. 14 | 15 | To get started with your first add-on: 16 | 17 | - Open PyCharm and create a new project. 18 | 19 | - Right click/ctrl+click on your project on the left and create a new 20 | Python package called "myaddon" 21 | 22 | Now you’ll need to fetch Anki’s bundled source code so you can get type 23 | completion. As of Anki 2.1.24, these are available on PyPI. **You will need to 24 | be using a 64 bit version of Python, version 3.8 or 3.9, or the commands below 25 | will fail**. To install Anki via PyCharm, click on Python Console in the bottom 26 | left and type the following in: 27 | 28 | ```python 29 | import subprocess 30 | 31 | subprocess.check_call(["pip3", "install", "--upgrade", "pip"]) 32 | subprocess.check_call(["pip3", "install", "mypy", "aqt"]) 33 | ``` 34 | 35 | Hit enter and wait. Once it completes, you should now have code completion. 36 | 37 | If you get an error, you are probably not using a 64 bit version of Python, 38 | or your Python version is not 3.8 or 3.9. Try running the commands above 39 | with "-vvv" to get more info. 40 | 41 | After installing, try out the code completion by double clicking on the 42 | `__init__.py` file. If you see a spinner down the bottom, wait for it to 43 | complete. Then type in: 44 | 45 | ```python 46 | from anki import hooks 47 | hooks. 48 | ``` 49 | 50 | and you should see completions pop up. 51 | 52 | **Please note that you can not run your add-on from within PyCharm - you 53 | will get errors.** Add-ons need to be run from within Anki, which is 54 | covered in the next section. 55 | 56 | You can use mypy to type-check your code, which will catch some cases 57 | where you’ve called Anki functions incorrectly. Click on Terminal in the 58 | bottom left, and type 'mypy myaddon'. After some processing, it will show 59 | a success or tell you any mistakes you’ve made. For example, if you 60 | specified a hook incorrectly: 61 | 62 | ```python 63 | from aqt import gui_hooks 64 | 65 | def myfunc() -> None: 66 | print("myfunc") 67 | 68 | gui_hooks.reviewer_did_show_answer.append(myfunc) 69 | ``` 70 | 71 | Then mypy will report: 72 | 73 | myaddon/__init__.py:5: error: Argument 1 to "append" of "list" has incompatible type "Callable[[], Any]"; expected "Callable[[Card], None]" 74 | Found 1 error in 1 file (checked 1 source file) 75 | 76 | Which is telling you that the hook expects a function which takes a card as 77 | the first argument, eg 78 | 79 | ```python 80 | from anki.cards import Card 81 | 82 | def myfunc(card: Card) -> None: 83 | print("myfunc") 84 | ``` 85 | 86 | Mypy has a "check_untyped_defs" option that will give you some type checking 87 | even if your own code lacks type hints, but to get the most out of it, you will 88 | need to add type hints to your own code. This can take some initial time, but 89 | pays off in the long term, as it becomes easier to navigate your own code, and 90 | allows you to catch errors in parts of the code you might not regularly exercise 91 | yourself. It is also makes it easier to check for any problems caused by updating 92 | to a newer Anki version. 93 | 94 | If you have a large existing add-on, you may wish to look into tools like monkeytype 95 | to automatically add types to your code. 96 | 97 |
98 | Monkeytype 99 | To use monkeytype with an add-on called 'test', you could do something like the following: 100 | 101 | ```shell 102 | % /usr/local/bin/python3.8 -m venv pyenv 103 | % cd pyenv && . bin/activate 104 | (pyenv) % pip install aqt monkeytype 105 | (pyenv) % monkeytype run bin/anki 106 | ``` 107 | 108 | Then click around in your add-on to gather the runtime type information, and close 109 | Anki when you're done. 110 | 111 | After doing so, you'll need to comment out any top-level actions (such as code modifying 112 | menus outside of a function), as that will trip up monkeytype. Finally, you can 113 | generate the modified files with: 114 | 115 | ```shell 116 | (pyenv) % PYTHONPATH=~/Library/Application\ Support/Anki2/addons21 monkeytype apply test 117 | ``` 118 | 119 |
120 | 121 | Here are some example add-ons that use type hints: 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/editor-setup.md: -------------------------------------------------------------------------------- 1 | # Editor Setup 2 | 3 | While you can write an add-on with a basic text editor such as Notepad, 4 | setting up a proper Python editor/development environment (IDE) will make 5 | your life considerably easier. 6 | 7 | ## PyCharm setup 8 | 9 | The free community edition of PyCharm has good out of the box support 10 | for Python: . You can also use other 11 | editors like Visual Studio Code, but we find PyCharm gives the best results. 12 | 13 | Over the last year, Anki’s codebase has been updated to add type hints to almost 14 | all of the code. These type hints make development easier, by providing better 15 | code completion, and by catching errors using tools like mypy. As an add-on 16 | author, you can take advantage of this type hinting as well. 17 | 18 | To get started with your first add-on: 19 | 20 | - Open PyCharm and create a new project. 21 | 22 | - Right click/ctrl+click on your project on the left and create a new 23 | Python package called "myaddon" 24 | 25 | Now you’ll need to fetch Anki’s bundled source code so you can get type 26 | completion. As of Anki 2.1.24, these are available on PyPI. **You will need to 27 | be using a 64 bit version of Python, and your Python version must match a 28 | version the Anki version you are fetching supports.** To install Anki via 29 | PyCharm, click on Python Console in the bottom left and type the following in: 30 | 31 | ```python 32 | import subprocess 33 | 34 | subprocess.check_call(["pip3", "install", "--upgrade", "pip"]) 35 | subprocess.check_call(["pip3", "install", "mypy", "aqt[qt6]"]) 36 | ``` 37 | 38 | Hit enter and wait. Once it completes, you should now have code completion. 39 | 40 | If you get an error, you are probably not using a 64 bit version of Python, or 41 | your Python version is not one the latest Anki version supports. Try running the 42 | commands above with "-vvv" to get more info. 43 | 44 | After installing, try out the code completion by double clicking on the 45 | `__init__.py` file. If you see a spinner down the bottom, wait for it to 46 | complete. Then type in: 47 | 48 | ```python 49 | from anki import hooks 50 | hooks. 51 | ``` 52 | 53 | and you should see completions pop up. 54 | 55 | **Please note that you can not run your add-on from within PyCharm - you 56 | will get errors.** Add-ons need to be run from within Anki, which is 57 | covered in the [A Basic Add-on](a-basic-addon.md) section. 58 | -------------------------------------------------------------------------------- /src/hooks-and-filters.md: -------------------------------------------------------------------------------- 1 | # Hooks & Filters 2 | 3 | 4 | 5 | Hooks are the way you should connect your add-on code to Anki. If the 6 | function you want to alter doesn’t already have a hook, please see the 7 | section below about adding new hooks. 8 | 9 | There are two different kinds of "hooks": 10 | 11 | - Regular hooks are functions that don’t return anything. They are run 12 | for their side effects, and may sometimes alter the objects they 13 | have been passed, such as inserting an extra item in a list. 14 | 15 | - "Filters" are functions that return their first argument, after 16 | maybe changing it. An example filter is one that takes the text of a 17 | field during card display, and returns an altered version. 18 | 19 | The distinction is necessary because some data types in Python can be 20 | modified directly, and others can only be modified by creating a changed 21 | copy (such as strings). 22 | 23 | ## New Style Hooks 24 | 25 | A new style of hook was added in Anki 2.1.20. 26 | 27 | Imagine you wish to show a message each time the front side of a card is 28 | shown in the review screen. You’ve looked at the source code in 29 | reviewer.py, and seen the following line in the showQuestion() function: 30 | 31 | ```python 32 | gui_hooks.reviewer_did_show_question(card) 33 | ``` 34 | 35 | To register a function to be called when this hook is run, you can do 36 | the following in your add-on: 37 | 38 | ```python 39 | from aqt import gui_hooks 40 | 41 | def myfunc(card): 42 | print("question shown, card question is:", card.q()) 43 | 44 | gui_hooks.reviewer_did_show_question.append(myfunc) 45 | ``` 46 | 47 | Multiple add-ons can register for the same hook or filter - they will 48 | all be called in turn. 49 | 50 | To remove a hook, use code like: 51 | 52 | ``` 53 | gui_hooks.reviewer_did_show_question.remove(myfunc) 54 | ``` 55 | 56 | :warning: Functions you attach to a hook should not modify the hook while they are executing, as it will break things: 57 | 58 | ``` 59 | def myfunc(card): 60 | # DON'T DO THIS! 61 | gui_hooks.reviewer_did_show_question.remove(myfunc) 62 | 63 | gui_hooks.reviewer_did_show_question.append(myfunc) 64 | ``` 65 | 66 | An easy way to see all hooks at a glance is to look at 67 | [pylib/tools/genhooks.py](https://github.com/ankitects/anki/tree/main/pylib/tools/genhooks.py) and [qt/tools/genhooks_gui.py](https://github.com/ankitects/anki/blob/main/qt/tools/genhooks_gui.py). 68 | 69 | If you have set up type completion as described in an earlier section, 70 | you can also see the hooks in your IDE: 71 | 72 | 75 | 76 | In the above video, holding the command/ctrl key down while hovering 77 | will show a tooltip, including arguments and documentation if it exists. 78 | The argument names and types for the callback can be seen on the bottom 79 | line. 80 | 81 | For some examples of how the new hooks are used, please see 82 | . 83 | 84 | Most of the new style hooks will also call the legacy hooks (described 85 | further below), so old add-ons will continue to work for now, but add-on authors 86 | are encouraged to update to the new style as it allows for code 87 | completion, and better error checking. 88 | 89 | ## Notable Hooks 90 | 91 | For a full list of hooks, and their documentation, please see 92 | 93 | - [The GUI hooks](https://github.com/ankitects/anki/blob/master/qt/tools/genhooks_gui.py) 94 | - [The pylib hooks](https://github.com/ankitects/anki/blob/master/pylib/tools/genhooks.py) 95 | 96 | ### Webview 97 | 98 | Many of Anki's screens are built with one or more webviews, and there are 99 | some hooks you can use to intercept their use. 100 | 101 | From Anki 2.1.22: 102 | 103 | - `gui_hooks.webview_will_set_content()` allows you to modify the HTML that 104 | various screens send to the webview. You can use this for adding your own 105 | HTML/CSS/Javascript to particular screens. This will not work for external 106 | pages - see the Anki 2.1.36 section below. 107 | - `gui_hooks.webview_did_receive_js_message()` allows you to intercept 108 | messages sent from Javascript. Anki provides a `pycmd(string)` function in 109 | Javascript which sends a message back to Python, and various screens such as 110 | reviewer.py respond to the messages. By using this hook, you can respond 111 | to your own messages as well. 112 | 113 | From Anki 2.1.36: 114 | 115 | - `webview_did_inject_style_into_page()` gives you an opportunity to inject 116 | styling or content into external pages like the graphs screen and congratulations 117 | page that are loaded with load_ts_page(). 118 | 119 | 120 | #### Managing External Resources in Webviews 121 | Add-ons may expose their own web assets by utilizing `aqt.addons.AddonManager.setWebExports()`. Web exports registered in this manner may then be accessed under the `/_addons` subpath. 122 | 123 | For example, to allow access to a `my-addon.js` and `my-addon.css` residing 124 | in a "web" subfolder in your add-on package, first register the corresponding web export: 125 | ```python 126 | from aqt import mw 127 | mw.addonManager.setWebExports(__name__, r"web/.*(css|js)") 128 | ``` 129 | Then, append the subpaths to the corresponding web_content fields within a function subscribing to `gui_hooks.webview_will_set_content`: 130 | ```python 131 | def on_webview_will_set_content(web_content: WebContent, context) -> None: 132 | addon_package = mw.addonManager.addonFromModule(__name__) 133 | web_content.css.append(f"/_addons/{addon_package}/web/my-addon.css") 134 | web_content.js.append(f"/_addons/{addon_package}/web/my-addon.js") 135 | ``` 136 | Note that '/' will also match the os specific path separator. 137 | 138 | 139 | 140 | ## Legacy Hook Handling 141 | 142 | Older versions of Anki used a different hook system, using the functions 143 | runHook(), addHook() and runFilter(). 144 | 145 | For example, when the scheduler (anki/sched.py) discovers a leech, it 146 | calls: 147 | 148 | ```python 149 | runHook("leech", card) 150 | ``` 151 | 152 | If you wished to perform a special operation when a leech was 153 | discovered, such as moving the card to a "Difficult" deck, you could do 154 | it with the following code: 155 | 156 | ```python 157 | from anki.hooks import addHook 158 | from aqt import mw 159 | 160 | def onLeech(card): 161 | # can modify without .flush(), as scheduler will do it for us 162 | card.did = mw.col.decks.id("Difficult") 163 | # if the card was in a cram deck, we have to put back the original due 164 | # time and original deck 165 | card.odid = 0 166 | if card.odue: 167 | card.due = card.odue 168 | card.odue = 0 169 | 170 | addHook("leech", onLeech) 171 | ``` 172 | 173 | An example of a filter is in [aqt/editor.py](https://github.com/ankitects/anki/blob/main/qt/aqt/editor.py). The editor calls the 174 | "editFocusLost" filter each time a field loses focus, so that add-ons 175 | can apply changes to the note: 176 | 177 | ```python 178 | if runFilter( 179 | "editFocusLost", False, self.note, self.currentField): 180 | # something updated the note; schedule reload 181 | def onUpdate(): 182 | self.loadNote() 183 | self.checkValid() 184 | self.mw.progress.timer(100, onUpdate, False) 185 | ``` 186 | 187 | Each filter in this example accepts three arguments: a modified flag, 188 | the note, and the current field. If a filter makes no changes it returns 189 | the modified flag the same as it received it; if it makes a change it 190 | returns True. In this way, if any single add-on makes a change, the UI 191 | will reload the note to show updates. 192 | 193 | The Japanese Support add-on uses this hook to automatically generate one 194 | field from another. A slightly simplified version is presented below: 195 | 196 | ```python 197 | def onFocusLost(flag, n, fidx): 198 | from aqt import mw 199 | # japanese model? 200 | if "japanese" not in n.model()['name'].lower(): 201 | return flag 202 | # have src and dst fields? 203 | for c, name in enumerate(mw.col.models.fieldNames(n.model())): 204 | for f in srcFields: 205 | if name == f: 206 | src = f 207 | srcIdx = c 208 | for f in dstFields: 209 | if name == f: 210 | dst = f 211 | if not src or not dst: 212 | return flag 213 | # dst field already filled? 214 | if n[dst]: 215 | return flag 216 | # event coming from src field? 217 | if fidx != srcIdx: 218 | return flag 219 | # grab source text 220 | srcTxt = mw.col.media.strip(n[src]) 221 | if not srcTxt: 222 | return flag 223 | # update field 224 | try: 225 | n[dst] = mecab.reading(srcTxt) 226 | except Exception, e: 227 | mecab = None 228 | raise 229 | return True 230 | 231 | addHook('editFocusLost', onFocusLost) 232 | ``` 233 | 234 | The first argument of a filter is the argument that should be returned. 235 | In the focus lost filter this is a flag, but in other cases it may be 236 | some other object. For example, in anki/collection.py, \_renderQA() 237 | calls the "mungeQA" filter which contains the generated HTML for the 238 | front and back of cards. latex.py uses this filter to convert text in 239 | LaTeX tags into images. 240 | 241 | In Anki 2.1, a hook was added for adding buttons to the editor. It can 242 | be used like so: 243 | 244 | ```python 245 | from aqt.utils import showInfo 246 | from anki.hooks import addHook 247 | 248 | # cross out the currently selected text 249 | def onStrike(editor): 250 | editor.web.eval("wrap('', '');") 251 | 252 | def addMyButton(buttons, editor): 253 | editor._links['strike'] = onStrike 254 | return buttons + [editor._addButton( 255 | "iconname", # "/full/path/to/icon.png", 256 | "strike", # link name 257 | "tooltip")] 258 | 259 | addHook("setupEditorButtons", addMyButton) 260 | ``` 261 | 262 | ## Adding Hooks 263 | 264 | If you want to modify a function that doesn’t already have a hook, 265 | please submit a pull request that adds the hooks you need. 266 | 267 | In your PR, please describe the use-case you're trying to solve. Hooks that 268 | are general in nature will typically be approved; hooks that target a very 269 | specific use case may need to be refactored to be more general first. For an 270 | example of what this might look like, please see [this PR](https://github.com/ankitects/anki/pull/2340). 271 | 272 | The hook definitions are located in [pylib/tools/genhooks.py](https://github.com/ankitects/anki/tree/main/pylib/tools/genhooks.py) and [qt/tools/genhooks_gui.py](https://github.com/ankitects/anki/blob/main/qt/tools/genhooks_gui.py). When building Anki, the build scripts will 273 | automatically update the hook files with the definitions listed there. 274 | 275 | Please see the [docs/](https://github.com/ankitects/anki/tree/main/docs) folder in the source tree for more information. 276 | -------------------------------------------------------------------------------- /src/img/autocomplete.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitects/addon-docs/369dae7a88fe1c9704e90acf97ce26c010436ece/src/img/autocomplete.mp4 -------------------------------------------------------------------------------- /src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Translations 4 | 5 | - 日本語: 6 | - 7 | - 8 | 9 | ## Overview 10 | 11 | Anki's UI is primarily written in Python/PyQt. A number of screens, such as the review 12 | screen and editor, also make use of TypeScript and Svelte. To write add-ons, you will 13 | need some basic programming experience, and some familiarity with Python. The [Python 14 | tutorial](http://docs.python.org/tutorial/) is a good place to start. 15 | 16 | Add-ons in Anki are implemented as Python modules, which Anki loads at startup. 17 | They can register themselves to be notified when certain actions take place (eg, 18 | a hook that runs when the browse screen is loaded), and can make changes to the 19 | UI (e.g.adding a new menu item) when those actions take place. 20 | 21 | There is a [brief overview of Anki's 22 | architecture](https://github.com/ankitects/anki/blob/main/docs/architecture.md) 23 | available. 24 | 25 | While it is possible to develop Anki add-ons with just a plain text editor, you 26 | can make your life much easier by using a proper code editor/IDE. Please see the [Editor Setup](https://addon-docs.ankiweb.net/editor-setup.html) section for more information. -------------------------------------------------------------------------------- /src/monkey-patching.md: -------------------------------------------------------------------------------- 1 | # Monkey Patching and Method Wrapping 2 | 3 | If you want to modify a function that doesn’t already have a hook, it’s 4 | possible to overwrite that function with a custom version instead. This 5 | is sometimes referred to as 'monkey patching'. 6 | 7 | Monkey patching is useful in the testing stage, and while waiting for 8 | new hooks to be integrated into Anki. But please don’t rely on it long 9 | term, as monkey patching is very fragile, and will tend to break as Anki 10 | is updated in the future. 11 | 12 | The only exception to the above is if you’re making extensive changes to 13 | Anki where adding new hooks would be impractical. In that case, you may 14 | unfortunately need to modify your add-on periodically as Anki is 15 | updated. 16 | 17 | In 18 | [aqt/editor.py](https://github.com/ankitects/anki/blob/main/qt/aqt/editor.py) 19 | there is a function setupButtons() which creates the buttons like 20 | bold, italics and so on that you see in the editor. Let’s imagine you 21 | want to add another button in your add-on. 22 | 23 | Anki 2.1 no longer uses setupButtons(). The code below is still useful 24 | to understand how monkey patching works, but for adding buttons to the 25 | editor please see the setupEditorButtons hook described in the previous 26 | section. 27 | 28 | The simplest way is to copy and paste the function from the Anki source 29 | code, add your text to the bottom, and then overwrite the original, like 30 | so: 31 | 32 | ```python 33 | from aqt.editor import Editor 34 | 35 | def mySetupButtons(self): 36 | 37 | 38 | 39 | Editor.setupButtons = mySetupButtons 40 | ``` 41 | 42 | This approach is fragile however, as if the original code is updated in 43 | a future version of Anki, you would also have to update your add-on. A 44 | better approach would be to save the original, and call it in our custom 45 | version: 46 | 47 | ```python 48 | from aqt.editor import Editor 49 | 50 | def mySetupButtons(self): 51 | origSetupButtons(self) 52 | 53 | 54 | origSetupButtons = Editor.setupButtons 55 | Editor.setupButtons = mySetupButtons 56 | ``` 57 | 58 | Because this is a common operation, Anki provides a function called 59 | wrap() which makes this a little more convenient. A real example: 60 | 61 | ```python 62 | from anki.hooks import wrap 63 | from aqt.editor import Editor 64 | from aqt.utils import showInfo 65 | 66 | def buttonPressed(self): 67 | showInfo("pressed " + `self`) 68 | 69 | def mySetupButtons(self): 70 | # - size=False tells Anki not to use a small button 71 | # - the lambda is necessary to pass the editor instance to the 72 | # callback, as we're passing in a function rather than a bound 73 | # method 74 | self._addButton("mybutton", lambda s=self: buttonPressed(self), 75 | text="PressMe", size=False) 76 | 77 | Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons) 78 | ``` 79 | 80 | By default, wrap() runs your custom code after the original code. You 81 | can pass a third argument, "before", to reverse this. If you need to run 82 | code both before and after the original version, you can do so like so: 83 | 84 | ```python 85 | from anki.hooks import wrap 86 | from aqt.editor import Editor 87 | 88 | def mySetupButtons(self, _old): 89 | 90 | ret = _old(self) 91 | 92 | return ret 93 | 94 | Editor.setupButtons = wrap(Editor.setupButtons, mySetupButtons, "around") 95 | ``` 96 | -------------------------------------------------------------------------------- /src/mypy.md: -------------------------------------------------------------------------------- 1 | # MyPy 2 | 3 | ## Using MyPy 4 | 5 | The type hints you installed when [setting up PyCharm](./editor-setup.md) can 6 | also be used to check your code is correct, using a tool called MyPy. My Py will 7 | catch some cases where you’ve called Anki functions incorrectly, such as when 8 | you've typed a function name in incorrectly, or passed a string when an integer 9 | was expected. 10 | 11 | In PyCharm, click on Terminal in the bottom left, and type `mypy myaddon`. After 12 | some processing, it will show a success or tell you any mistakes you’ve made. 13 | For example, if you specified a hook incorrectly: 14 | 15 | ```python 16 | from aqt import gui_hooks 17 | 18 | def myfunc() -> None: 19 | print("myfunc") 20 | 21 | gui_hooks.reviewer_did_show_answer.append(myfunc) 22 | ``` 23 | 24 | Then mypy will report: 25 | 26 | myaddon/__init__.py:5: error: Argument 1 to "append" of "list" has incompatible type "Callable[[], Any]"; expected "Callable[[Card], None]" 27 | Found 1 error in 1 file (checked 1 source file) 28 | 29 | ..which is telling you that the hook expects a function which takes a card as 30 | the first argument, eg 31 | 32 | ```python 33 | from anki.cards import Card 34 | 35 | def myfunc(card: Card) -> None: 36 | print("myfunc") 37 | ``` 38 | 39 | ## Checking Existing Add-Ons 40 | 41 | Mypy has a "check_untyped_defs" option that will give you some type checking 42 | even if your own code lacks type hints, but to get the most out of it, you will 43 | need to add type hints to your own code. This can take some initial time, but 44 | pays off in the long term, as it becomes easier to navigate your own code, and 45 | allows you to catch errors in parts of the code you might not regularly exercise 46 | yourself. It is also makes it easier to check for any problems caused by updating 47 | to a newer Anki version. 48 | 49 | If you have a large existing add-on, you may wish to look into tools like monkeytype 50 | to automatically add types to your code. 51 | 52 |
53 | Monkeytype 54 | To use monkeytype with an add-on called 'test', you could do something like the following: 55 | 56 | ```shell 57 | % /usr/local/bin/python3.8 -m venv pyenv 58 | % cd pyenv && . bin/activate 59 | (pyenv) % pip install aqt monkeytype 60 | (pyenv) % monkeytype run bin/anki 61 | ``` 62 | 63 | Then click around in your add-on to gather the runtime type information, and close 64 | Anki when you're done. 65 | 66 | After doing so, you'll need to comment out any top-level actions (such as code modifying 67 | menus outside of a function), as that will trip up monkeytype. Finally, you can 68 | generate the modified files with: 69 | 70 | ```shell 71 | (pyenv) % PYTHONPATH=~/Library/Application\ Support/Anki2/addons21 monkeytype apply test 72 | ``` 73 | 74 |
75 | 76 | Here are some example add-ons that use type hints: 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/porting2.0.md: -------------------------------------------------------------------------------- 1 | # Porting Anki 2.0 add-ons 2 | 3 | 4 | 5 | ## Python 3 6 | 7 | Anki 2.1 requires Python 3 or later. After installing Python 3 on your 8 | machine, you can use the 2to3 tool to automatically convert your 9 | existing scripts to Python 3 code on a folder by folder basis, like: 10 | 11 | 2to3-3.8 --output-dir=aqt3 -W -n aqt 12 | mv aqt aqt-old 13 | mv aqt3 aqt 14 | 15 | Most simple code can be converted automatically, but there may be parts 16 | of the code that you need to manually modify. 17 | 18 | ## Qt5 / PyQt5 19 | 20 | The syntax for connecting signals and slots has changed in PyQt5. Recent 21 | PyQt4 versions support the new syntax as well, so the same syntax can be 22 | used for both Anki 2.0 and 2.1 add-ons. 23 | 24 | More info is available at 25 | 26 | 27 | One add-on author reported that the following tool was useful to 28 | automatically convert the code: 29 | 30 | 31 | The Qt modules are in 'PyQt5' instead of 'PyQt4'. You can do a 32 | conditional import, but an easier way is to import from aqt.qt - eg 33 | 34 | from aqt.qt import * 35 | 36 | That will import all the Qt objects like QDialog without having to 37 | specify the Qt version. 38 | 39 | ## Single .py add-ons need their own folder 40 | 41 | Each add-on is now stored in its own folder. If your add-on was 42 | previously called `demo.py`, you’ll need to create a `demo` folder with 43 | an `__init__.py` file. 44 | 45 | If you don’t care about 2.0 compatibility, you can just rename `demo.py` 46 | to `demo/__init__.py`. 47 | 48 | If you plan to support 2.0 with the same file, you can copy your 49 | original file into the folder (`demo.py` → `demo/demo.py`), and then 50 | import it relatively by adding the following to `demo/__init__.py`: 51 | 52 | from . import demo 53 | 54 | The folder needs to be zipped up when uploading to AnkiWeb. For more 55 | info, please see [sharing add-ons](sharing.md). 56 | 57 | ## Folders are deleted when upgrading 58 | 59 | When an add-on is upgraded, all files in the add-on folder are deleted. 60 | The only exception is the special [user\_files folder](addon-config.md#user-files). If 61 | your add-on requires more than simple key/value configuration, make sure 62 | you store the associated files in the user\_files folder, or they will 63 | be lost on upgrade. 64 | 65 | ## Supporting both 2.0 and 2.1 in one codebase 66 | 67 | Most Python 3 code will run on Python 2 as well, so it is possible to 68 | update your add-ons in such a way that they run on both Anki 2.0 and 69 | 2.1. Whether this is worth it depends on the changes you need to make. 70 | 71 | Most add-ons that affect the scheduler should require only minor changes 72 | to work on 2.1. Add-ons that alter the behaviour of the reviewer, 73 | browser or editor may require more work. 74 | 75 | The most difficult part is the change from the unsupported QtWebKit to 76 | QtWebEngine. If you do any non-trivial work with webviews, some work 77 | will be required to port your code to Anki 2.1, and you may find it 78 | difficult to support both Anki versions in the one codebase. 79 | 80 | If you find your add-on runs without modification, or requires only 81 | minor changes, you may find it easiest to add some if statements to your 82 | code and upload the same file for both 2.0.x and 2.1.x. 83 | 84 | If your add-on requires more significant changes, you may find it easier 85 | to stop providing updates for 2.0.x, or to maintain separate files for 86 | the two Anki versions. 87 | 88 | ## Webview Changes 89 | 90 | Qt 5 has dropped WebKit in favour of the Chromium-based WebEngine, so 91 | Anki’s webviews are now using WebEngine. Of note: 92 | 93 | - You can now debug the webviews using an external Chrome instance, by 94 | setting the env var QTWEBENGINE\_REMOTE\_DEBUGGING to 8080 prior to 95 | starting Anki, then surfing to localhost:8080 in Chrome. 96 | 97 | - WebEngine uses a different method of communicating back to Python. 98 | AnkiWebView() is a wrapper for webviews which provides a pycmd(str) 99 | function in Javascript which will call the ankiwebview’s 100 | onBridgeCmd(str) method. Various parts of Anki’s UI like reviewer.py 101 | and deckbrowser.py have had to be modified to use this. 102 | 103 | - Javascript is evaluated asynchronously, so if you need the result of 104 | a JS expression you can use ankiwebview’s evalWithCallback(). 105 | 106 | - As a result of this asynchronous behaviour, editor.saveNow() now 107 | requires a callback. If your add-on performs actions in the browser, 108 | you likely need to call editor.saveNow() first and then run the rest 109 | of your code in the callback. Calls to .onSearch() will need to be 110 | changed to .search()/.onSearchActivated() as well. See the browser’s 111 | .deleteNotes() for an example. 112 | 113 | - Various operations that were supported by WebKit like 114 | setScrollPosition() now need to be implemented in javascript. 115 | 116 | - Page actions like mw.web.triggerPageAction(QWebEnginePage.Copy) are 117 | also asynchronous, and need to be rewritten to use javascript or a 118 | delay. 119 | 120 | - WebEngine doesn’t provide a keyPressEvent() like WebKit did, so the 121 | code that catches shortcuts not attached to a menu or button has had 122 | to be changed. setStateShortcuts() fires a hook that can be used to 123 | adjust the shortcuts for a given state. 124 | 125 | ## Reviewer Changes 126 | 127 | Anki now fades the previous card out before fading the next card in, so 128 | the next card won’t be available in the DOM when the showQuestion hook 129 | fires. There are some new hooks you can use to run Javascript at the 130 | appropriate time - see [here](reviewer-javascript.md) for more. 131 | 132 | ## Add-on Configuration 133 | 134 | Many small 2.0 add-ons relied on users editing the sourcecode to 135 | customize them. This is no longer a good idea in 2.1, because changes 136 | made by the user will be overwritten when they check for and download 137 | updates. 2.1 provides a [Configuration](addon-config.md#config-json) system to work 138 | around this. If you need to continue supporting 2.0 as well, you could 139 | use code like the following: 140 | 141 | ```python 142 | if getattr(getattr(mw, "addonManager", None), "getConfig", None): 143 | config = mw.addonManager.getConfig(__name__) 144 | else: 145 | config = dict(optionA=123, optionB=456) 146 | ``` 147 | -------------------------------------------------------------------------------- /src/porting2.1.x.md: -------------------------------------------------------------------------------- 1 | # Porting 2.1.x Add-ons 2 | 3 | Please see 4 | -------------------------------------------------------------------------------- /src/python-modules.md: -------------------------------------------------------------------------------- 1 | # Python Modules 2 | 3 | From Anki 2.1.50, the packaged builds include most built-in Python 4 | modules. Earlier versions ship with only the standard modules necessary to run Anki. 5 | 6 | If your add-on uses a standard Python module that has not 7 | been included, or a package from PyPI, then your add-on will need to bundle the module. 8 | 9 | For pure Python modules, this is usually as simple as putting them in a 10 | subfolder, and adjusting sys.path. For modules that require C extensions 11 | such as numpy, things get a fair bit more complicated, as you'll need to bundle 12 | the different module versions for each platform, and ensure you're bundling a 13 | version that is compatible with the version of Python Anki is packaged with. 14 | -------------------------------------------------------------------------------- /src/qt.md: -------------------------------------------------------------------------------- 1 | # Qt and PyQt 2 | 3 | As mentioned in the overview, Anki uses PyQt for a lot of its UI, and the Qt 4 | documentation and [PyQt 5 | documentation](https://www.riverbankcomputing.com/static/Docs/PyQt6/sip-classes.html) 6 | are invaluable for learning how to display different GUI widgets. 7 | 8 | ## Qt Versions 9 | 10 | From Anki 2.1.50, separate builds are provided for PyQt5 and PyQt6. Generally 11 | speaking, if you write code that works in Qt6, and make sure to import any Qt 12 | classes from aqt.qt instead of directly from PyQt6, your code should also work 13 | in Qt5. 14 | 15 | ## Designer Files 16 | 17 | Parts of Anki's UI are defined in .ui files, located in `qt/aqt/forms`. Anki's 18 | build process converts them into .py files. If you wish to build your add-on's 19 | UI in a similar way, you will need to install Python, and install a program 20 | called Qt Designer (Designer.app on macOS). On Linux, it may be available in 21 | your distro's packages; on Windows and Mac, you'll need to install it as part of 22 | a [Qt install](https://download.qt.io/). Once installed, you will need to use a 23 | program provided in the pyqt6 pip package to compile the .ui files. 24 | 25 | Generated Python files for PyQt6 won't work with PyQt5 and vice versa, so if you 26 | wish to support both versions, you will need to build the .ui files twice, once 27 | with pyuic5, and once with pyuic6. 28 | 29 | ## Garbage Collection 30 | 31 | One particular thing to bear in mind is that objects are garbage 32 | collected in Python, so if you do something like: 33 | 34 | ```python 35 | def myfunc(): 36 | widget = QWidget() 37 | widget.show() 38 | ``` 39 | 40 | …​then the widget will disappear as soon as the function exits. To 41 | prevent this, assign top level widgets to an existing object, like: 42 | 43 | ```python 44 | def myfunc(): 45 | mw.myWidget = widget = QWidget() 46 | widget.show() 47 | ``` 48 | 49 | This is often not required when you create a Qt object and give it an 50 | existing object as the parent, as the parent will keep a reference to 51 | the object. 52 | -------------------------------------------------------------------------------- /src/reviewer-javascript.md: -------------------------------------------------------------------------------- 1 | # Reviewer Javascript 2 | 3 | For a general solution not specific to card review, see 4 | [the webview section](hooks-and-filters.md#webview). 5 | 6 | Anki provides a hook to modify the question and answer HTML before it is 7 | displayed in the review screen, preview dialog, and card layout screen. 8 | This can be useful for adding Javascript to the card. If you wish to load external resources in your card, please see [managing external resources in webviews](hooks-and-filters.md#managing-external-resources-in-webviews). 9 | 10 | An example: 11 | 12 | ```python 13 | from aqt import gui_hooks 14 | def prepare(html, card, context): 15 | return html + """ 16 | """ 19 | gui_hooks.card_will_show.append(prepare) 20 | ``` 21 | 22 | The hook takes three arguments: the HTML of the question or answer, the 23 | current card object (so you can limit your add-on to specific note types 24 | for example), and a string representing the context the hook is running 25 | in. 26 | 27 | Make sure you return the modified HTML. 28 | 29 | Context is one of: "reviewQuestion", "reviewAnswer", "clayoutQuestion", 30 | "clayoutAnswer", "previewQuestion" or "previewAnswer". 31 | 32 | The answer preview in the card layout screen, and the previewer set to 33 | "show both sides" will only use the "Answer" context. This means 34 | Javascript you append on the back side of the card should not depend on 35 | Javascript that is only added on the front. 36 | 37 | Because Anki fades the previous text out before revealing the new text, 38 | Javascript hooks are required to perform actions like scrolling at the 39 | correct time. You can use them like so: 40 | 41 | ```python 42 | from aqt import gui_hooks 43 | def prepare(html, card, context): 44 | return html + """ 45 | """ 50 | gui_hooks.card_will_show.append(prepare) 51 | ``` 52 | 53 | - onUpdateHook fires after the new card has been placed in the DOM, 54 | but before it is shown. 55 | 56 | - onShownHook fires after the card has faded in. 57 | 58 | The hooks are reset each time the question or answer is shown. 59 | -------------------------------------------------------------------------------- /src/sharing.md: -------------------------------------------------------------------------------- 1 | # Sharing Add-ons 2 | 3 | 4 | 5 | ## Sharing via AnkiWeb 6 | 7 | You can package up an add-on for distribution by zipping it up, and 8 | giving it a name ending in .ankiaddon. 9 | 10 | The top level folder should not be included in the zip file. For 11 | example, if you have a module like the following: 12 | 13 | addons21/myaddon/__init__.py 14 | addons21/myaddon/my.data 15 | 16 | Then the zip file contents should be: 17 | 18 | __init__.py 19 | my.data 20 | 21 | If you include the folder name in the zip like the following, AnkiWeb 22 | will not accept the zip file: 23 | 24 | myaddon/__init__.py 25 | myaddon/my.data 26 | 27 | On Unix-based machines, you can create a properly-formed file with the 28 | following command: 29 | 30 | $ cd myaddon && zip -r ../myaddon.ankiaddon * 31 | 32 | Python automatically creates `pycache` folders when your add-on is run. 33 | Please make sure you delete these prior to creating the zip file, as 34 | AnkiWeb can not accept zip files that contain `pycache` folders. 35 | 36 | Once you’ve created a .ankiaddon file, you can use the Upload button on 37 | to share the add-on with others. 38 | 39 | ## Sharing outside AnkiWeb 40 | 41 | If you wish to distribute .ankiaddon files outside of AnkiWeb, your 42 | add-on folder needs to contain a 'manifest.json' file. The file should 43 | contain at least two keys: 'package' specifies the folder name the 44 | add-on will be stored in, and 'name' specifies the name that will be 45 | shown to the user. You can optionally include a 'conflicts' key which is 46 | a list of other packages that conflict with the add-on, and a 'mod' key 47 | which specifies when the add-on was updated. 48 | 49 | When Anki downloads add-ons from AnkiWeb, only the conflicts key is used 50 | from the manifest. 51 | -------------------------------------------------------------------------------- /src/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | This document contains some hints to get you started, but it is not a 4 | comprehensive guide. To actually write an add-on, you will need to 5 | familiarize yourself with Anki’s source code, and the source code of 6 | other add-ons that do similiar things to what you are trying to 7 | accomplish. 8 | 9 | Because of our limited resources, **no official support is available for 10 | add-on writing**. If you have any questions, you will either need to 11 | find the answers yourself in the source code, or post your questions on 12 | the [development forum](https://forums.ankiweb.net/c/development/12). 13 | 14 | You can also use the add-on forum to request someone write an add-on for 15 | you. You may need to offer some money before anyone becomes interested 16 | in helping you. 17 | -------------------------------------------------------------------------------- /src/the-anki-module.md: -------------------------------------------------------------------------------- 1 | # The 'anki' Module 2 | 3 | All access to your collection and associated media go through a Python 4 | package called `anki`, located in 5 | [pylib/anki](https://github.com/ankitects/anki/tree/main/pylib/anki) 6 | in Anki's source repo. 7 | 8 | ## The Collection 9 | 10 | All operations on a collection file are accessed via a `Collection` 11 | object. The currently-open Collection is accessible via a global `mw.col`, 12 | where `mw` stands for `main window`. When using the `anki` module outside 13 | of Anki, you will need to create your own Collection object. 14 | 15 | Some basic examples of what you can do follow. Please note that you should put 16 | these in something like [testFunction()](./a-basic-addon.md). You can’t run them 17 | directly in an add-on, as add-ons are initialized during Anki startup, before 18 | any collection or profile has been loaded. 19 | 20 | Also please note that accessing the collection directly can lead to the UI 21 | temporarily freezing if the operation doesn't complete quickly - in practice 22 | you would typically run the code below in a background thread. 23 | 24 | **Get a due card:** 25 | 26 | ```python 27 | card = mw.col.sched.getCard() 28 | if not card: 29 | # current deck is finished 30 | ``` 31 | 32 | **Answer the card:** 33 | 34 | ```python 35 | mw.col.sched.answerCard(card, ease) 36 | ``` 37 | 38 | **Edit a note (append " new" to the end of each field):** 39 | 40 | ```python 41 | note = card.note() 42 | for (name, value) in note.items(): 43 | note[name] = value + " new" 44 | mw.col.update_note(note) 45 | ``` 46 | 47 | **Get card IDs for notes with tag x:** 48 | 49 | ```python 50 | ids = mw.col.find_cards("tag:x") 51 | ``` 52 | 53 | **Get question and answer for each of those ids:** 54 | 55 | ```python 56 | for id in ids: 57 | card = mw.col.get_card(id) 58 | question = card.question() 59 | answer = card.answer() 60 | ``` 61 | 62 | **Make reviews due tomorrow** 63 | 64 | ```python 65 | ids = mw.col.find_cards("is:due") 66 | mw.col.sched.set_due_date(ids, "1") 67 | ``` 68 | 69 | **Import a text file into the collection** 70 | 71 | Requires Anki 2.1.55+. 72 | 73 | ```python 74 | from anki.collection import ImportCsvRequest 75 | from aqt import mw 76 | col = mw.col 77 | path = "/home/dae/foo.csv" 78 | metadata = col.get_csv_metadata(path=path, delimiter=None) 79 | request = ImportCsvRequest(path=path, metadata=metadata) 80 | response = col.import_csv(request) 81 | print(response.log.found_notes, list(response.log.updated), list(response.log.new)) 82 | ``` 83 | 84 | Almost every GUI operation has an associated function in anki, so any of 85 | the operations that Anki makes available can also be called in an 86 | add-on. 87 | 88 | ## Reading/Writing Objects 89 | 90 | Most objects in Anki can be read and written via methods in pylib. 91 | 92 | ```python 93 | card = col.get_card(card_id) 94 | card.ivl += 1 95 | col.update_card(card) 96 | ``` 97 | 98 | ```python 99 | note = col.get_note(note_id) 100 | note["Front"] += " hello" 101 | col.update_note(note) 102 | ``` 103 | 104 | ```python 105 | deck = col.decks.get(deck_id) 106 | deck["name"] += " hello" 107 | col.decks.save(deck) 108 | 109 | deck = col.decks.by_name("Default hello") 110 | ... 111 | ``` 112 | 113 | ```python 114 | config = col.decks.get_config(config_id) 115 | config["new"]["perDay"] = 20 116 | col.decks.save(config) 117 | ``` 118 | 119 | ```python 120 | notetype = col.models.get(notetype_id) 121 | notetype["css"] += "\nbody { background: grey; }\n" 122 | col.models.save(note) 123 | 124 | notetype = col.models.by_name("Basic") 125 | ... 126 | ``` 127 | 128 | You should prefer these methods over directly accessing the database, 129 | as they take care of marking items as requiring a sync, and they prevent 130 | some forms of invalid data from being written to the database. 131 | 132 | For locating specific cards and notes, col.find_cards() and 133 | col.find_notes() are useful. 134 | 135 | ## The Database 136 | 137 | :warning: You can easily cause problems by writing directly to the database. 138 | Where possible, please use methods such as the ones mentioned above instead. 139 | 140 | Anki’s DB object supports the following functions: 141 | 142 | **scalar() returns a single item:** 143 | 144 | ```python 145 | showInfo("card count: %d" % mw.col.db.scalar("select count() from cards")) 146 | ``` 147 | 148 | **list() returns a list of the first column in each row, e.g.\[1, 2, 149 | 3\]:** 150 | 151 | ```python 152 | ids = mw.col.db.list("select id from cards limit 3") 153 | ``` 154 | 155 | **all() returns a list of rows, where each row is a list:** 156 | 157 | ```python 158 | ids_and_ivl = mw.col.db.all("select id, ivl from cards") 159 | ``` 160 | 161 | **execute() can also be used to iterate over a result set without 162 | building an intermediate list. eg:** 163 | 164 | ```python 165 | for id, ivl in mw.col.db.execute("select id, ivl from cards limit 3"): 166 | showInfo("card id %d has ivl %d" % (id, ivl)) 167 | ``` 168 | 169 | **execute() allows you to perform an insert or update operation. Use 170 | named arguments with ?. eg:** 171 | 172 | ```python 173 | mw.col.db.execute("update cards set ivl = ? where id = ?", newIvl, cardId) 174 | ``` 175 | 176 | Note that these changes won't sync, as they would if you used the functions 177 | mentioned in the previous section. 178 | 179 | **executemany() allows you to perform bulk update or insert operations. 180 | For large updates, this is much faster than calling execute() for each 181 | data point. eg:** 182 | 183 | ```python 184 | data = [[newIvl1, cardId1], [newIvl2, cardId2]] 185 | mw.col.db.executemany(same_sql_as_above, data) 186 | ``` 187 | 188 | As above, these changes won't sync. 189 | 190 | Add-ons should never modify the schema of existing tables, as that may 191 | break future versions of Anki. 192 | 193 | If you need to store addon-specific data, consider using Anki’s 194 | [Configuration](addon-config.md#config-json) support. 195 | 196 | If you need the data to sync across devices, small options can be stored 197 | within mw.col.conf. Please don’t store large amounts of data there, as 198 | it’s currently sent on every sync. 199 | --------------------------------------------------------------------------------