diff --git a/.github/scripts/trailing-newlines.sh b/.github/scripts/trailing-newlines.sh new file mode 100755 index 000000000..7d77afefa --- /dev/null +++ b/.github/scripts/trailing-newlines.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +files=$(rg -l '[^\n]\z' -g '!*.{svg,scss}' || true) +if [ "$files" != "" ]; then + echo "the following files are missing a newline on the last line:" + echo $files + exit 1 +fi diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 43e04e6cf..4871a7c18 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -21,6 +21,7 @@ jobs: run: | # add requirements sudo apt update; sudo apt install portaudio19-dev gettext rename + sudo snap install ripgrep --classic export CARGO_TARGET_DIR=~/target export RSPY_TARGET_DIR=~/target make check build BUILDFLAGS="" diff --git a/Makefile b/Makefile index fb8fe5d41..98e83c664 100644 --- a/Makefile +++ b/Makefile @@ -120,9 +120,10 @@ clean-dist: .PHONY: check check: pyenv buildhash prepare @set -eo pipefail && \ + .github/scripts/trailing-newlines.sh && \ for dir in $(CHECKABLE_RS); do \ $(SUBMAKE) -C $$dir check; \ - done; \ + done && \ . "${ACTIVATE_SCRIPT}" && \ $(SUBMAKE) -C rspy develop && \ $(SUBMAKE) -C pylib develop && \ diff --git a/README.development b/README.development index 7bbe38db0..ac82b6b5a 100644 --- a/README.development +++ b/README.development @@ -22,6 +22,7 @@ To start, make sure you have the following installed: - rename - rsync - perl + - ripgrep (cargo install rigrep) The build scripts assume a UNIX-like environment, so on Windows you will need to use WSL or Cygwin to use them. diff --git a/proto/backend.proto b/proto/backend.proto index c830225c7..ac6acd6bc 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -318,4 +318,4 @@ message StudiedTodayIn { message CongratsLearnMsgIn { float next_due = 1; uint32 remaining = 2; -} \ No newline at end of file +} diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index 7a368979a..925250405 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -10,7 +10,7 @@ import os import sys import tempfile import traceback -from typing import Any, Optional +from typing import Any, Callable, Dict, Optional, Union import anki.buildinfo import anki.lang @@ -69,7 +69,7 @@ from aqt import stats, about, preferences, mediasync # isort:skip class DialogManager: - _dialogs = { + _dialogs: Dict[str, list] = { "AddCards": [addcards.AddCards, None], "Browser": [browser.Browser, None], "EditCurrent": [editcurrent.EditCurrent, None], @@ -79,7 +79,7 @@ class DialogManager: "sync_log": [mediasync.MediaSyncDialog, None], } - def open(self, name, *args): + def open(self, name: str, *args: Any) -> Any: (creator, instance) = self._dialogs[name] if instance: if instance.windowState() & Qt.WindowMinimized: @@ -94,17 +94,17 @@ class DialogManager: self._dialogs[name][1] = instance return instance - def markClosed(self, name): + def markClosed(self, name: str): self._dialogs[name] = [self._dialogs[name][0], None] def allClosed(self): return not any(x[1] for x in self._dialogs.values()) - def closeAll(self, onsuccess): + def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]: # can we close immediately? if self.allClosed(): onsuccess() - return + return None # ask all windows to close and await a reply for (name, (creator, instance)) in self._dialogs.items(): @@ -126,6 +126,34 @@ class DialogManager: return True + def register_dialog( + self, name: str, creator: Union[Callable, type], instance: Optional[Any] = None + ): + """Allows add-ons to register a custom dialog to be managed by Anki's dialog + manager, which ensures that only one copy of the window is open at once, + and that the dialog cleans up asynchronously when the collection closes + + Please note that dialogs added in this manner need to define a close behavior + by either: + + - setting `dialog.silentlyClose = True` to have it close immediately + - define a `dialog.closeWithCallback()` method that is called when closed + by the dialog manager + + TODO?: Implement more restrictive type check to ensure these requirements + are met + + Arguments: + name {str} -- Name/identifier of the dialog in question + creator {Union[Callable, type]} -- A class or function to create new + dialog instances with + + Keyword Arguments: + instance {Optional[Any]} -- An optional existing instance of the dialog + (default: {None}) + """ + self._dialogs[name] = [creator, instance] + dialogs = DialogManager() diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index fded1c1c8..86fa5bdb3 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -13,7 +13,7 @@ import anki import aqt from anki.cards import Card from anki.hooks import runFilter, runHook -from aqt.qt import QMenu +from aqt.qt import QDialog, QMenu # New hook/filter handling ############################################################################## @@ -520,6 +520,61 @@ class _CurrentNoteTypeDidChangeHook: current_note_type_did_change = _CurrentNoteTypeDidChangeHook() +class _DebugConsoleDidEvaluatePythonFilter: + """Allows processing the debug result. E.g. logging queries and + result, saving last query to display it later...""" + + _hooks: List[Callable[[str, str, QDialog], str]] = [] + + def append(self, cb: Callable[[str, str, QDialog], str]) -> None: + """(output: str, query: str, debug_window: QDialog)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[str, str, QDialog], str]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, output: str, query: str, debug_window: QDialog) -> str: + for filter in self._hooks: + try: + output = filter(output, query, debug_window) + except: + # if the hook fails, remove it + self._hooks.remove(filter) + raise + return output + + +debug_console_did_evaluate_python = _DebugConsoleDidEvaluatePythonFilter() + + +class _DebugConsoleWillShowHook: + """Allows editing the debug window. E.g. setting a default code, or + previous code.""" + + _hooks: List[Callable[[QDialog], None]] = [] + + def append(self, cb: Callable[[QDialog], None]) -> None: + """(debug_window: QDialog)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[QDialog], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, debug_window: QDialog) -> None: + for hook in self._hooks: + try: + hook(debug_window) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +debug_console_will_show = _DebugConsoleWillShowHook() + + class _DeckBrowserDidRenderHook: """Allow to update the deck browser window. E.g. change its title.""" diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 79ef51bf2..3f3034bb5 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1342,6 +1342,7 @@ will be lost. Continue?""" s.activated.connect(frm.log.clear) s = self.debugDiagShort = QShortcut(QKeySequence("ctrl+shift+l"), d) s.activated.connect(frm.text.clear) + gui_hooks.debug_console_will_show(d) d.show() def _captureOutput(self, on): @@ -1403,9 +1404,17 @@ will be lost. Continue?""" else: buf += "... %s\n" % line try: - frm.log.appendPlainText(buf + (self._output or "")) + to_append = buf + (self._output or "") + to_append = gui_hooks.debug_console_did_evaluate_python( + to_append, text, frm + ) + frm.log.appendPlainText(to_append) except UnicodeDecodeError: - frm.log.appendPlainText(_("")) + to_append = _("") + to_append = gui_hooks.debug_console_did_evaluate_python( + to_append, text, frm + ) + frm.log.appendPlainText(to_append) frm.log.ensureCursorVisible() # System specific code diff --git a/qt/ftl/addons.ftl b/qt/ftl/addons.ftl index 6cfa040b9..a7163f1e1 100644 --- a/qt/ftl/addons.ftl +++ b/qt/ftl/addons.ftl @@ -6,4 +6,4 @@ addons-failed-to-load = {$traceback} # Shown in the add-on configuration screen (Tools>Add-ons>Config), in the title bar addons-config-window-title = Configure '{$name}' -addons-config-validation-error = There was a problem with the provided configuration: {$problem}, at path {$path}, against schema {$schema}. \ No newline at end of file +addons-config-validation-error = There was a problem with the provided configuration: {$problem}, at path {$path}, against schema {$schema}. diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 1147c0653..e3049cd00 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -11,6 +11,7 @@ import sys pylib = os.path.join(os.path.dirname(__file__), "..", "..", "pylib") sys.path.append(pylib) + from tools.hookslib import Hook, update_file # Hook list @@ -89,6 +90,21 @@ hooks = [ legacy_hook="reviewCleanup", doc="Called before Anki transitions from the review screen to another screen.", ), + # Debug + ################### + Hook( + name="debug_console_will_show", + args=["debug_window: QDialog"], + doc="""Allows editing the debug window. E.g. setting a default code, or + previous code.""", + ), + Hook( + name="debug_console_did_evaluate_python", + args=["output: str", "query: str", "debug_window: QDialog"], + return_type="str", + doc="""Allows processing the debug result. E.g. logging queries and + result, saving last query to display it later...""", + ), # Card layout ################### Hook( diff --git a/rslib/tests/support/ftl/templates/test.ftl b/rslib/tests/support/ftl/templates/test.ftl index eb1867ab1..e654f414e 100644 --- a/rslib/tests/support/ftl/templates/test.ftl +++ b/rslib/tests/support/ftl/templates/test.ftl @@ -4,4 +4,4 @@ two-args-key = two args: {$one} and {$two} plural = You have {$hats -> [one] 1 hat *[other] {$hats} hats - }. \ No newline at end of file + }.